What type of exception to throw in Spring RestController when validation fails?
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:
@ControllerAdvicepublic 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.