When creating a REST controller with Spring Boot it's possible to use Bean Validation to validate the methods arguments annotating the class with @Validated.
Then Bean Validation annotations can be used directly on the argument or, if the argument is a complex class with properties that must be validated (such as the request body), using @Valid.
The inconsistency I found was while handling violation on those validations.
While using the annotations from javax.validation.constraints (the validations from the Bean Validation API) the violations are handled by the @Validated handler (MethodValidationInterceptor) and throws ConstraintViolationException.
When using @Valid the violation is handled by ModelAttributeMethodProcessor and throws MethodArgumentNotValidException.
See the problem?
The @Validated, a Spring annotation that says it is a "Variant of JSR-303's Valid" and "Designed for convenient use with Spring's JSR-303 support but not JSR-303 specific" (see it here), throws ConstraintViolationException, an exception from the Bean Validation API (JSR-303).
And the opposite also is true, the @Valid (from Bean Validation API) throws MethodArgumentNotValidException (Spring exception).
I think it would be more concise if their behavior changed between them.
Does it makes sense?
Comment From: philwebb
I've transferred this issue to the spring-framework team since the code in question is part of spring-webmvc.
Comment From: ah1508
There is also the BindException, thrown by spring mvc if the invalid object has been built from the request parameters.
exemple : GET /books?title=&author.lastname= calls a method which declares a @Valid Book parameter (query by example),where title property is @NotBlank.
This forces to handle BindException and MethodNotValidArgumentException, but the code is the same. For example :
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
return e.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
}
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onBindException(BindException e) {
return e.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
}
a @ValidationFailureHandler could help to unify these common behaviors.
Comment From: sbrannen
@ah1508,
This forces to handle
BindExceptionandMethodNotValidArgumentException, but the code is the same. For example :
I was going to say you can handle both exceptions at the same time as follows...
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onValidationFailureException(Exception ex) {
return ((CommonBindingResultInterface) ex).getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
}
... but then I realized that MethodArgumentNotValidException and BindException do not share a common super type that declares the getBindingResult() method.
So one option to make this easier would be to introduce a common interface (like the CommonBindingResultInterface in the above example) that declares BindingResult getBindingResult(), and then MethodArgumentNotValidException and BindException could both implement that interface.
Comment From: rstoyanchev
@rafafael03 I've read your description several times and I can't be sure I understand what you mean and there are claims that aren't true.
First you can use either @Valid or @Validated interchangeably. They result in the same handling and do not raise different exceptions. The main difference for Spring MVC applications is that @Validated allows you to specify validation groups (hints). Also ModelAttributeMethodProcessor does not and throw MethodArgumentNotValidException but BindException.
What @ah1508 has written is correct. Validation through either of these annotations is supported for @ModelAttribute and for @RequestBody arguments. The former raises BindException but only if you don't handle it in the controller method by declaring a BindingResult argument. The latter raises MethodArgumentNotValidException.
Is this the same as your point?
As for finding a way to consolidate, perhaps MethodArgumentNotValidException could be made to extend BindException. They both have the same internal state more or less.
Comment From: ah1508
Indeed, if MethodArgumentNotValidException extends BindException, only one @ExceptionHandler would be needed.
Comment From: sbrannen
Tentatively slated for 5.3 for pending design work and assessment of viability regarding the proposal to have MethodArgumentNotValidException extend BindException.