Environment
Spring Boot: 3.2.0 Spring: 6.1 Java: 21
Expected Behavior:
If a controller method parameter is invalid, a HandlerMethodValidationException
should be thrown.
Observed Behavior:
If you attach a @Valid
annotation to a parameter, a MethodArgumentNotValidException
is thrown instead of a HandlerMethodValidationException
.
Reproduce the error
Custom ExceptionHandler
@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
throw new IllegalStateException("should not be thrown?", ex);
}
@Override
protected ResponseEntity<Object> handleHandlerMethodValidationException(HandlerMethodValidationException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
// ex.visitResults(); I want to visit RequestBody?
return new ResponseEntity<>(
Map.of("validation_error", ex.getMessage()),
HttpStatus.BAD_REQUEST
);
}
}
REST Controller
@RestController
public class ValidController {
@PostMapping(
value = "/test",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Object> test(@Valid @RequestBody Body body) {
return ResponseEntity.ok(Map.of("valid", true));
}
public record Body(
@NotNull
@Positive
@JsonProperty("number")
Integer number
) {
}
}
Make request
curl --location 'http://localhost:8080/test' \
--header 'Content-Type: application/json' \
--data '{
"number": -1
}'
Debugging Insights
In org.springframework.web.method.support.InvocableHandlerMethod
, method invokeForRequest(...)
:
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
Class<?>[] groups = getValidationGroups();
if (shouldValidateArguments() && this.methodValidator != null) {
this.methodValidator.applyArgumentValidation(
getBean(), getBridgedMethod(), getMethodParameters(), args, groups);
}
Object returnValue = doInvoke(args);
if (shouldValidateReturnValue() && this.methodValidator != null) {
this.methodValidator.applyReturnValueValidation(
getBean(), getBridgedMethod(), getReturnType(), returnValue, groups);
}
return returnValue;
}
The first line,
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
throws a MethodArgumentNotValidException
right before the handler validation should be started:
if (shouldValidateArguments() && this.methodValidator != null) {
,
where methodValidator
is org.springframework.web.method.annotation.HandlerMethodValidator
↓ ↓
MethodArgumentNotValidException
is thrown by org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
in method resolveArgument(...)
:
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
Additionally
I also tried with @Validated
.
I assume that the validation of controller method parameters, which currently throws exception MethodArgumentNotValidException
, is deprecated and should be replaced. When is the new validation method introduced in version 6.1 expected to replace it? I might be mistaken in my understanding.
Comment From: rstoyanchev
Method validation is applied as an additional layer, only if necessary. If all you have is a single @Valid
@ModelAttribute
or @RequestBody
it is validated through Bean Validation as an individual object and results in a MethodArgumentNotValidException
with a single BindingResult
. This has been in place for a long time and continues to work the same way.
Built-in method validation (new in 6.1) applies when any @Constraint
annotations are placed directly on controller method parameters. The only way to apply Bean Validation in that case is through method validation. This results in a HandlerMethodValidationException
with separate ParameterValidationResult
for each parameter, and among those there may be a ParameterErrors
(essentially a BindingResult
) for @RequestBody
or @ModelAttribute
.
In short this is expected behavior, and you'll need to handle both, but the error information that each exposes is aligned and should be comparable.
Comment From: unwx
Thank you for the explanation!
In extreme cases, if it is necessary to ensure that MethodArgumentNotValidException
was thrown due to a violation of a field in the @RequestBody
entity, ex.getParameter().getParameterAnnotation(RequestBody.class)
can be used for this purpose.
I have no questions/comments left.