Version
- spring framework: 6.1.1 (spring boot 3.2.0)
Situation
I tested below spring MVC rest controller and model. (use kotlin)
@RestController
@RequestMapping(value = ["/api"])
class FooController {
@PostExchange("/foo")
fun createFoo(
@RequestBody @Valid myData: MyData,
@Min(1) @RequestParam("hint") hint: Int
): Map<String, Any?> {
return mapOf("myData" to myData)
}
}
data class MyData(
val name: String,
@get:Min(1)
val age: Int
)
And call api and debug.
> curl -X POST \
-H 'Content-Type: application/json' \
'http://localhost:8080/api/foo?hint=2' \
-d '{"name": "foo-1", "age": 3}'
Then, the validation of age
field occurs twice.
Is this behavior valid?
Related code
In invokeForRequest
method, executed getMethodArgumentValues
method first and then methodValidator.applyArgumentValidation
method.
If requestBody is validate object and built-in method validation is activated, then request body object is validated twice.
https://github.com/spring-projects/spring-framework/blob/61be452402938090b104ba35501021012610d95d/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java#L171
https://github.com/spring-projects/spring-framework/blob/61be452402938090b104ba35501021012610d95d/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java#L178
Comment From: snicoll
Then, the validation of age field occurs twice.
How did you come to this conclusion?
Comment From: Crain-32
I can somewhat confirm this same behavior, but for a different reason. (Spring Boot 3.1.3, but this is Spring Framework level behavior)
The following Controller does reproduce the issue I'm describing. It appears the DataBinder
does an initial check before it converts the request to a body, and throws a MethodArgumentInvalidException
if it fails. Then it also calls the Jakarta Validator. If it fails in this stage we get a ConstraintViolationException
.
@Validated
@RestController
@RequestMapping("/rest/test")
public class TestController {
public record MyData(String foo, @NotNull String bar) {}
@PostMapping("/list")
public String sayHello(@Valid @RequestBody @NotEmpty List<@Valid MyData> myList) {
return "Hello";
}
@PostMapping
public String sayWorld(@Valid @RequestBody @NotNull MyData myData) {
return "World";
}
}
Sending in the following to /rest/test/list
will result in a ConstraintViolationException
[
{ "bar": null, "foo" : "buzz" }
]
Where sending this to the /rest/test
will result in a MethodArgumentInvalidException
{"bar": null, "foo": "buzz"}
I'm not 100% confident in the degree this Exception difference and the Double validation are related, but it does feel like they lay on a similar issue.
Comment From: giger85
@snicoll
Using Intellij (IDEA), I specified break point to AbstractMinValidator
as below
And, attached partial stack traces
- first validation at getMethodArgumentValues()
method
at org.hibernate.validator.internal.constraintvalidators.bv.number.bound.AbstractMinValidator.isValid(AbstractMinValidator.java:34)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:180)
at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:66)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:75)
at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:130)
at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:123)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:555)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:518)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:488)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:450)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:400)
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:172)
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:105)
at org.springframework.boot.autoconfigure.validation.ValidatorAdapter.validate(ValidatorAdapter.java:67)
at org.springframework.validation.DataBinder.validate(DataBinder.java:1247)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.validateIfApplicable(AbstractMessageConverterMethodArgumentResolver.java:252)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:141)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:218)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:171)
...
- second validation at
methodValidator.applyArgumentValidation()
method
at org.hibernate.validator.internal.constraintvalidators.bv.number.bound.AbstractMinValidator.isValid(AbstractMinValidator.java:34)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateSingleConstraint(ConstraintTree.java:180)
at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:66)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:75)
at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:130)
at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:123)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:555)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:518)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:488)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:450)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:400)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateCascadedAnnotatedObjectForCurrentGroup(ValidatorImpl.java:629)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateCascadedConstraints(ValidatorImpl.java:590)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateParametersInContext(ValidatorImpl.java:880)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:283)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:235)
at org.springframework.validation.beanvalidation.MethodValidationAdapter.invokeValidatorForArguments(MethodValidationAdapter.java:260)
at org.springframework.validation.beanvalidation.MethodValidationAdapter.validateArguments(MethodValidationAdapter.java:240)
at org.springframework.web.method.annotation.HandlerMethodValidator.validateArguments(HandlerMethodValidator.java:115)
at org.springframework.web.method.annotation.HandlerMethodValidator.applyArgumentValidation(HandlerMethodValidator.java:83)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:178)
Comment From: rstoyanchev
@giger85 this is not expected, but I will need to debug to find out more. Do you have a small sample?
Comment From: rstoyanchev
@Crain-32 your case is a little different. First you have a class-level @Validated
which applies method validation via AOP. That's exactly what we tried to solve in 6.1 with the Spring Web built-in method validation, removing the need to use AOP for this case. Please, check the reference docs. Beyond that there may be an issue with the example signatures that you shared. For example where constraint annotations are placed directly on the method parameter for @RequestBody
. At the moment, we expect that to be mostly @Valid
and that may be causing an issue, I'll need to debug to find out more.
Comment From: rstoyanchev
I was able to confirm the issue. It's related to the Spring Boot VaildatorAdapter
that wraps the jakarta.validation.Validator
and precludes us from recognizing it as such. The change for #31082 unwraps before checking so we correctly determine if method validation applies. However, there is an one more check in DefaulDataBinderFactory
that should exclude bean validation at the DataBinder
level, for @RequestBody
and @ModelAttribute
arguments, if method validation applies.
@Crain-32 for your case you only need to remove @Validated
from the class level in order to turn off validation through an AOP proxy, and rely on the Spring web, built-in method validation (new in 6.1) instead.
Comment From: giger85
@rstoyanchev Thanks for your support.