How to handle MethodArgumentNotValidException in Spring Boot

How to handle MethodArgumentNotValidException in Spring Boot

Error handling in Spring Boot is simple, easy, and efficient. Yet it can be daunting sometimes as different types of validation exceptions and annotations are supported by the Spring framework. In this tutorial, we go over how to handle MethodArgumentNotValidException in Spring Boot. Additionally, we discuss a similar exception known as ConstraintViolationException and highlight the differences between them and how to handle each properly.

What is MethodArgumentNotValidException

The MethodArgumentNotValidException is raised when Jakarta (formerly known as Javax) validation fails at the controller level.

Let’s say we have a controller as follows that allows clients to create an actor,

package com.madadipouya.redis.springdata.example.controller

import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer
import com.madadipouya.redis.springdata.example.model.Actor
import com.madadipouya.redis.springdata.example.model.Movie
import com.madadipouya.redis.springdata.example.service.ActorService
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Past
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.http.HttpStatus
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import java.time.LocalDate

@RestController
@RequestMapping("/v1/actors")
class ActorController(val actorService: ActorService) {

    @PostMapping
    @ResponseStatus(HttpStatus.ACCEPTED)
    private fun createActor(@Valid @RequestBody actor: ActorDto): Actor = actorService.createActor(actor)

    data class ActorDto(
        @get:NotBlank(message = "First name cannot be empty") val firstName: String,
        @get:NotBlank(message = "Last name cannot be empty") val lastName: String,
        @field:DateTimeFormat(pattern = "yyyy-MM-dd")
        @field:JsonDeserialize(using = LocalDateDeserializer::class)
        @field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
        @get:Past(message = "Provide date in yyyy-MM-dd format in past time") val birthDate: LocalDate
    )
}

As you can see, we have some validation (@NotBlank and @Past) annotations with custom messages. If a request does not fulfill what is expected (not empty first, last name, or birthday in the past date), then Spring Boot will raise the MethodArgumentNotValidException.

How to ensure validation works

To ensure the validation works, one must add one of the implementations of the JSR-303, JSR-349, or JSR-380 specification to the project. That can be Jakarta validation or Spring Boot validation which uses Hibernate validator under the hood.

For simplicity’s sake, we stick to the Spring Boot validator. Make sure it exists in your project,

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

In addition to the validation library, you should add either Jakarta’s @Valid or Spring’s @Validated annotation. The former is suited for method-level validation. The latter fits the class level.

For the controller code above, we used @Valid as we are interested in validating a single method input.

What is the difference between ConstraintViolationException and MethodArgumentNotValidException

The ConstraintViolationException is raised by Hibernate entity manager when some constraints are violated. In other words, the ConstraintViolationException is raised by Hibernate when validation of any classes annotated with @Entity fails. On the other hand, the MethodArgumentNotValidException is thrown when validating controller input fails.

Handling MethodArgumentNotValidException in Spring Boot

The easiest and most effective approach to handle the MethodArgumentNotValidException is to utilize the @ExceptionHandler annotation and retrieve failures and cascade them as a response body so that we can inform the client about it.

@ExceptionHandler(value = [MethodArgumentNotValidException::class])
fun handleMethodArgumentValidationExceptions(
    exception: MethodArgumentNotValidException,
    webRequest: WebRequest
): ResponseEntity<ApiError> {
    val errors = exception.bindingResult.fieldErrors.map { fieldError ->
        "${fieldError.field}: ${fieldError.defaultMessage}"
    }
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiError(errors))
}

data class ApiError(val errors: List<String>) {
    constructor(error: String) : this(listOf(error))
}

In the above code, we retrieve the field name and the error message, then transform them to the proper ApiError class and send it as the response body.

Note that we cannot handle both MethodArgumentNotValidException and ConstraintViolationException with a single method. Each must have a dedicated handler.

For the ConstraintViolationException, one can handle it as below,

@ExceptionHandler(value = [ConstraintViolationException::class])
fun handleConstraintViolationExceptions(
    exception: ConstraintViolationException,
    webRequest: WebRequest
): ResponseEntity<ApiError> {
    val errors = exception.constraintViolations.map { violation -> violation.message }
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiError(errors))
}

Conclusion

In this tutorial, we discussed how to handle MethodArgumentNotValidException in Spring Boot. We started by explaining what the MethodArgumentNotValidException is and how to ensure controller validation works. Then we address the differences between MethodArgumentNotValidException and ConstraintViolationException. Finally, we cover how to properly handle MethodArgumentNotValidException and ConstraintViolationException by creating two different exception handlers. The sample project code is available on GitHub: https://github.com/kasramp/spring-data-redis-example-kotlin.

Looking for more Spring Boot content? Check here.

Inline/featured images credits