Sunday, September 22, 2013

Different ways of validating @RequestBody in Spring MVC with @Valid annotation

In Spring MVC the @RequestBody annotation indicates a method parameter should be bound to a body of the request. @RequestBody parameter can treated as any other parameter in a @RequestMapping method and therefore it can also be validated by a standard validation mechanism.

In this post I will show 3 ways of validating the @RequestBody parameter in your Spring MVC application.

Note: This blog post got a solid update (30/03/2016). The source code got migrated to new repository and it is Spring Boot driven. If you still prefer classic Spring MVC application you may use my spring-mvc-quickstart-archetype

Task @RestController

In my sample application I want to create a new Task with non blank name and description. In order to do it I create an API endpoint that supports POST method and accepts Task JSON object.

Let’s start with the task

public class Task {

    @NotBlank(message = "Task name must not be blank!")
    private String name;

    @NotBlank(message = "Task description must not be blank!")
    private String description;

    // getters and setters

}

To handle the task we need a @RestController:

@RestController // since Spring 4.0
@RequestMapping(value = "task")
public class TaskController {

    @RequestMapping(value = "", method = RequestMethod.POST)
    public Task post(Task task) {
        // create a task   
    }
}

ValidationError object

Before we jump into a validation let’s create the ValidationError object that holds validation errors:

public class ValidationError {

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<String> errors = new ArrayList<>();

    private final String errorMessage;

    public ValidationError(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public void addValidationError(String error) {
        errors.add(error);
    }

    public List<String> getErrors() {
        return errors;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}

The sample JSON error:

{
  "errorMessage": "Validation failed. 1 error(s)",
  "errors": [
    "Task name must not be blank!"
  ]
}

The ValidationError can be easily created from BindingResult. We may need a simple helper in order to do so:

public class ValidationErrorBuilder {

    public static ValidationError fromBindingErrors(Errors errors) {
        ValidationError error = new ValidationError("Validation failed. " + errors.getErrorCount() + " error(s)");
        for (ObjectError objectError : errors.getAllErrors()) {
            error.addValidationError(objectError.getDefaultMessage());
        }
        return error;
    }
}

The next thing we need to do is the actual validation. So let’s do it.

Validation with @ExceptionHandler

As of Spring 3.1 the @RequestBody method argument can be annotated with @Valid or @Validated annotation to invoke automatic validation.

In such a case Spring automatically performs the validation and in case of error MethodArgumentNotValidException is thrown.

Optional @ExceptionHandler method may be easily created to add custom behavior for handling this type of exception. MethodArgumentNotValidException holds both the parameter that failed the validation and the result of validation.

Now, we can easily extract error messages and return it in an error object as JSON.

@RestController
@RequestMapping("task")
public class TaskController {

    @RequestMapping(value = "", method = RequestMethod.POST)
    public Task createTask(@Valid @RequestBody Task task) {
        return task;
    }

    @ExceptionHandler
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public ValidationError handleException(MethodArgumentNotValidException exception) {
        return createValidationError(exception);
    }

    private ValidationError createValidationError(MethodArgumentNotValidException e) {
        return ValidationErrorBuilder.fromBindingErrors(exception.getBindingResult());
    }
}

Note: Exception handler method does not need to be located in the same controller class. It can be a global handler for all you API calls.

Validation with @ControllerAdvice

@ControllerAdvice is a specialization of a @Component that is used to define @ExceptionHandler, @InitBinder, and @ModelAttribute methods that apply to all @RequestMapping methods.

As of Spring 4 @ControllerAdvice may be configured to support defined subset of controllers, whereas the default behavior can be still utilized.

Note: If you want to learn more read this article: @ControllerAdvice improvements in Spring 4

To assist only TaskController, we may create the following @ControllerAdvice:

@ControllerAdvice(assignableTypes = TaskController2.class)
public class TaskController2Advice extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException exception, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ValidationError error = ValidationErrorBuilder.fromBindingErrors(exception.getBindingResult());
        return super.handleExceptionInternal(exception, error, headers, status, request);
    }
}

Validation with Errors/BindingResult object

As of Spring 3.2 @RequestBody method argument may be followed by Errors object, hence allowing handling of validation errors in the same @RequestMapping. Let’s look at the code:

@RestController
@RequestMapping("task")
public class TaskController3 {

    @RequestMapping(value = "", method = RequestMethod.POST)
    public ResponseEntity createTask(@Valid @RequestBody Task task, Errors errors) {
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(ValidationErrorBuilder.fromBindingErrors(errors));
        }
        return ResponseEntity.ok(task);
    }
}

Integration testing

All three approaches produce exactly the same result in case of validation error. We can quickly check that using an integration test. For each solution the same test will pass.

Note: The below code uses org.skyscreamer.jsonassert.JSONAssert. As of Spring 4.1 it is much easier to test JSON content in integration tests.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class TaskControllerTest2 {

    @Autowired
    private WebApplicationContext wac;
    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    @Test
    public void validRequestReturns200OK() throws Exception {
        String jsonTask = String.format("{\"name\": \"Task 1\",\"description\": \"Description\"}");

        this.mockMvc.perform(post("task")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonTask))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().json(jsonTask)); // Spring 4.1. Requires org.skyscreamer.jsonassert.JSONAssert
    }

    @Test
    public void invalidNameError() throws Exception {
        String jsonTaskWithBlankName = String.format("{\"name\": \"\",\"description\": \"Description\"}");

        this.mockMvc.perform(post("task")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonTaskWithBlankName))
                    .andDo(print())
                    .andExpect(status().isBadRequest())
                    .andExpect(content().json("{\"errors\":[\"Task name must not be blank!\"],\"errorMessage\":\"Validation failed. 1 error(s)\"}"));

    }
}

Source code

The source code contains all three approaches, so that you can quickly compare them: https://github.com/kolorobot/spring-mvc-beanvalidation11-demo

Similar articles

In case you find this article interesting, have a look at my other blog posts:

11 comments:

  1. Replies
    1. "Extension of HttpEntity that adds a HttpStatus status code. Used in RestTemplate as well @Controller methods."

      http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/ResponseEntity.html

      Delete
  2. This article has been brought very helpful.
    Thank you very much!!^^

    ReplyDelete
  3. Hey thanks for the guide. What is ApiErrors? thanks

    ReplyDelete
    Replies
    1. ApiErrors is a POJO - an error container (custom)

      Delete
  4. Hi, i am new to spring mvc and i have a doubt, i hope you can help me please.
    I have a rest web service which makes crud operations with hibernate. in my client program I consume the service through forms (for example to insert a user) but i dont know how to validate a field and print the error on the form. How can I validate on server and print the error on client form?

    ReplyDelete
    Replies
    1. Not sure if I understand what you wish to achieve. What do you mean to consume WS via form?

      - If you have a classic MVC app with e.g. Thymeleaf see form validation here: http://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#validation-and-error-messages

      - If you have a Single Page App (e.g. Angular) - handle the error in the service and bind errors to the form.

      Delete
  5. Consider There is a Object subTask of typeSubTask under Task class, then How do I validate SubTask properties.

    ReplyDelete
  6. Hi, Thanks for posting this great article, just wanted to make a really little correction TaskController.createValidationError the argumanet should be named 'exception' instead of 'e'.

    ReplyDelete
  7. I would rather do:

    public static ValidationError fromBindingErrors(Errors errors) {
    ValidationError error = new ValidationError("Validation failed. " + errors.getErrorCount() + " error(s)");
    for (FieldError objectError : errors.getFieldErrors()) {
    error.addValidationError(objectError.getField()+": "+objectError.getDefaultMessage());
    }
    return error;
    }

    Thanks for the post, it's a lot of help.

    ReplyDelete
  8. Hi,

    ValidationErrorBuilder.fromBindingErrors(Error errors) {...}
    should be
    ValidationErrorBuilder.fromBindingErrors(BindingResults errors) {...}

    Kind regards,

    Dennis

    ReplyDelete