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

Spring-initializer

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.