19

In a Spring RestController I have an input validation of the RequestBody simply by annotating the corresponding method parameter as @Valid or @Validated. Some other validations can only be performed after some processing of the incoming data. My question is, what type of exceptions should I use, so that it resembles the exception thrown by the @Valid annotation, and how do I construct this exception from the validation result. Here is an example:

@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> createOrder(@RequestBody @Validated(InputChecks.class) Order order) {
    // Some processing of the Order goes here
    Set<ConstraintViolation<Order>> violations = validator.validate(order, FinalChecks.class);
    // What to do now with the validation errors?
    orders.put(order);
    HttpHeaders headers = new HttpHeaders();
    headers.setLocation(ServletUriComponentsBuilder.fromCurrentRequest().path("/" + order.getId()).build().toUri());
    return new ResponseEntity<>(null, headers, HttpStatus.CREATED);
}
Ph03n1x
  • 794
  • 6
  • 12
Gregor
  • 2,917
  • 5
  • 28
  • 50

2 Answers2

23

To me the simplest way looks like validating the object with an errors object, and use it in a MethodArgumentNotValidException.

@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> createOrder(@RequestBody @Validated(InputChecks.class) Order order)
                throws NoSuchMethodException, SecurityException, MethodArgumentNotValidException {
    // Some processing of the Order goes here
    SpringValidatorAdapter v = new SpringValidatorAdapter(validator);
    BeanPropertyBindingResult errors = new BeanPropertyBindingResult(order, "order");
    v.validate(order, errors, FinalChecks.class);
    if (errors.hasErrors()) {
        throw new MethodArgumentNotValidException(
                new MethodParameter(this.getClass().getDeclaredMethod("createOrder", Order.class), 0),
                errors);
    }
    orders.put(order);
    HttpHeaders headers = new HttpHeaders();
    headers.setLocation(ServletUriComponentsBuilder.fromCurrentRequest().path("/" + order.getId()).build().toUri());
    return new ResponseEntity<>(null, headers, HttpStatus.CREATED);
}

This way the errors found during the second validation step have exactly the same structure as the errors found during the input validation on the @validated parameters.

Gregor
  • 2,917
  • 5
  • 28
  • 50
  • 1
    Unless the ExceptionHandler code uses the methodparameter field while processing the error message, I feel we can simply pass null value for the MethodParameter field – Narasimha Feb 06 '17 at 15:47
  • I'll second what @Narasimha says, the MethodParameter is not very useful when constructing the form error response, just leave it null a) because ugly, b) it can cause an error if the signature changes. – Peter Hawkins Jan 04 '18 at 13:21
9

For handling validation errors in the second run, i can think of three different approaches. First, you can extract validation error messages from Set of ConstraintViolations and then return an appropriate HTTP response, say 400 Bad Request, with validation error messages as the response body:

Set<ConstraintViolation<Order>> violations = validator.validate(order, FinalChecks.class);
if (!violations.isEmpty()) {
    Set<String> validationMessages = violations
                                     .stream()
                                     .map(ConstraintViolation::getMessage)
                                     .collect(Collectors.toSet());

    return ResponseEntity.badRequest().body(validationMessages);
}
// the happy path

This approach is suitable for situations when the double validation is a requirement for a few controllers. Otherwise, it's better to throw a brand new Exception or reuse spring related exceptions, say MethodArgumentNotValidException, and define a ControllerAdvice that handle them universally:

Set<ConstraintViolation<Order>> violations = validator.validate(order, FinalChecks.class);
if (!violations.isEmpty()) {
    throw new ValidationException(violations);
}

And the controller advice:

@ControllerAdvice
public class ValidationControllerAdvice {
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity handleValidtionErrors(ValidationException ex) {
        return ResponseEntity.badRequest().body(ex.getViolations().stream()...);
    }
}

You can also throw one of spring exceptions like MethodArgumentNotValidException. In order to do so, you need to convert the Set of ConstraintViolations to an instance of BindingResult and pass it to the MethodArgumentNotValidException's constructor.

Ali Dehghani
  • 46,221
  • 15
  • 164
  • 151
  • Thanks for your answer. But in my case I actually have to validation runs. The first validation checks the input as it comes from the post request, and the second one is supposed to check the object again, this time with the changes and additions from my controller. Now the question is how do I deal with the validation errors of the second run. – Gregor Mar 03 '16 at 09:18
  • What `ValidationException` is that? There's no `javax.validation.ValidationException.getViolations()`... – OrangeDog Sep 09 '16 at 09:17
  • @OrangeDog It's a custom exception, kinda implied by `it's better to throw a brand new Exception...`. I agree I should be more explicit – Ali Dehghani Sep 09 '16 at 10:00
  • Oh I see. I thought you might have meant `o.s.v.BindException` or something. – OrangeDog Sep 09 '16 at 10:03