Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What type of exception to throw in Spring RestController when validation fails?

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);
}
like image 314
Gregor Avatar asked Mar 03 '16 08:03

Gregor


People also ask

Which exception is thrown when a PATH variable validation fails?

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).

How do you handle exceptions in Spring boot REST API?

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.


2 Answers

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.

like image 64
Gregor Avatar answered Sep 22 '22 23:09

Gregor


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.

like image 23
Ali Dehghani Avatar answered Sep 19 '22 23:09

Ali Dehghani