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);
}
If the Input class contains a field with another complex type that should be validated, this field, too, needs to be annotated with @Valid . If the validation fails, it will trigger a MethodArgumentNotValidException . By default, Spring will translate this exception to a HTTP status 400 (Bad Request).
Altogether, the most common way is to use @ExceptionHandler on methods of @ControllerAdvice classes so that the exception handling will be applied globally or to a subset of controllers. ControllerAdvice is an annotation introduced in Spring 3.2, and as the name suggests, is “Advice” for multiple controllers.
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.
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 ConstraintViolation
s 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 ConstraintViolation
s to an instance of BindingResult
and pass it to the MethodArgumentNotValidException
's constructor.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With