kamer.dev

Custom Javax Annotation Error Handling in Spring

- #spring - #java - #validation

Hello! In this article, I’m gonna cover how to override default exception handler method for MethodArgumentNotValidException which is thrown when Java Bean Validation Annotations are violated.

What is Java Bean Validation?

Java Bean Validation is a specification that is defined in JSR-380. It makes possible defining constraints with annotations, write custom constraints, etc. I will use Hibernate Validator since it is the only certified implementation of Bean Validation 2.0. It’s not a requirement to use with Spring but I’m going to implement it with Spring Boot because of its popularity. I will not explain how to use these annotations however you can find it in one of my articles.

Create a Simple Project and Provide Some Annotations

Create a project with Spring Initializr and select Web and Lombok dependencies. Because web-starter includes Hibernate Validator. Then create a class named User as below.

@Data
public class User {

    @Length(min = 2, max = 30, message = "Name must be between 2-30 characters. ")
    @Pattern(regexp = "^[a-zA-Z]+(([',. -][a-zA-Z ])?[a-zA-Z]*)*$", message = "Name is invalid.")
    private String name;

    @Length(min = 2, max = 30, message = "Surname must be between 2-30 characters.")
    @Pattern(regexp = "^[a-zA-Z]+(([',. -][a-zA-Z ])?[a-zA-Z]*)*$", message = "Surname is invalid.")
    private String surname;

    @Email(message = "Enter a valid email address.")
    private String email;

Using an external message source for error messages is better but I didn’t use it because it would make this article longer.

Then create a RestController and accept User as a @Valid input.

@RestController
@RequestMapping("/api/users")
public class UserRestController {

    @PostMapping
    public ResponseEntity<User> saveUser(@Valid @RequestBody User user) {

	return ResponseEntity.ok(user);
    }
}

Do not forget to use @Valid annotation. Because it makes sure that the annotated parameter is validated.

Run the application and send an example request. (You don’t have to use cURL of course. I sent this request with Postman. You can just copy this and import it as a raw text to Postman.)

curl --location --request POST 'http://localhost:8080/api/users' \
--header 'Content-Type: application/json' \
--data-raw '{
      "name": "Marion",
      "surname": "Cotillard",
      "email": "[email protected]",
      "birthdate": "1975-09-30"
}'

This request will return 200 as could be expected. Now let’s break some rules:

curl --location --request POST 'http://localhost:8080/api/users' \
--header 'Content-Type: application/json' \
--data-raw '{
      "name": "M",
      "surname": "Cotillard",
      "email": "[email protected]",
      "birthdate": "1975-09-30"
}'

I just broke @Length rule on name field. This request responded with a long response:

{
  "timestamp": "2020-02-01T21:27:06.935+0000",
  "status": 400,
  "error": "Bad Request",
  "errors": [
    {
      "codes": [
	"Length.user.name",
	"Length.name",
	"Length.java.lang.String",
	"Length"
      ],
      "arguments": [
	{
	  "codes": [
	    "user.name",
	    "name"
	  ],
	  "arguments": null,
	  "defaultMessage": "name",
	  "code": "name"
	},
	30,
	2
      ],
      "defaultMessage": "Name must be between 2-30 characters. ",
      "objectName": "user",
      "field": "name",
      "rejectedValue": "M",
      "bindingFailure": false,
      "code": "Length"
    }
  ],
  "message": "Validation failed for object='user'. Error count: 1",
  "path": "/api/users"
}

This can be a confusing response but it’s possible to create a custom response.

Override Exception Handler

Firstly, create a POJO for the custom response.

@Getter
@Setter
@Builder
@AllArgsConstructor
public class CustomFieldError {

      private String field;

      private String message;

}

You can add extra fields but this is enough for this article. Then create an exception handler method:

@ExceptionHandler(MethodArgumentNotValidException.class)
public final ResponseEntity<Object> handleUserMethodFieldErrors(MethodArgumentNotValidException ex, WebRequest request) {
      // some logic
}

Javax Annotations throws MethodArgumentNotValidException.class so, overrode this exception. In the method, extract field errors and create CustomFieldError objects from them.

final List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();

final List<CustomFieldError> customFieldErrors = new ArrayList<>();

for (FieldError fieldError : fieldErrors) {

      final String field = fieldError.getField();

      final String message = fieldError.getDefaultMessage();

      final CustomFieldError customFieldError = CustomFieldError.builder().field(field).message(message).build();

      customFieldErrors.add(customFieldError);

}

You can also get rejectedValue and errorCode. Then create an HTTP response and return.

return ResponseEntity.badRequest().body(customFieldErrors);

Run the project again and send the same request. This will return 400 response with the body below:

[
      {
	      "field": "name",
	      "message": "Name must be between 2-30 characters. "
      }
]

You can customise this response or override similar exception handlers with the same approach.

Github Repo: https://github.com/kamer/custom-javax-annotation-error-handling