Affects: 6.1 (tested with Spring Boot 3.2.3)
Until Spring Boot 3.1 (3.1.8 to be exact) following endpoint was fine:
@GetMapping("/foo/bar/{id}")
@NotNull ResponseEntity<@NotNull String> getById(@PathVariable @NotNull String id) {
...
Now this produces following error: {HV000197: No value extractor found for type parameter 'T' of type org.springframework.http.ResponseEntity.
If I remove the @NotNull
(=jakarta.validation.constraints.NotNull
) in the ResponseEntity
-template parameter the code works as successfully as before:
@GetMapping("/foo/bar/{id}")
@NotNull ResponseEntity<String> getById(@PathVariable @NotNull String id) {
...
If I test with other annotation types it works too (tested with jakarta.validation.constraints.NotBlank
or own annotation). So there must be something special with the @NotNull
annotation but I don't see a reason why the @NotNull
-annotation should produce this error? (It is an error from my point of view.)
Here the relevant stack trace:
<init>:17, ConstraintDeclarationException (jakarta.validation)
getNoValueExtractorFoundForTypeException:1609, Log_$logger (org.hibernate.validator.internal.util.logging)
addValueExtractorDescriptorForTypeArgumentLocation:147, MetaConstraints (org.hibernate.validator.internal.metadata.core)
create:63, MetaConstraints (org.hibernate.validator.internal.metadata.core)
findTypeUseConstraints:806, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
findTypeArgumentsConstraints:776, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
findTypeAnnotationConstraints:620, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
findExecutableMetaData:336, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
getMetaData:292, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
getMethodMetaData:279, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
retrieveBeanConfiguration:131, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
getBeanConfiguration:121, AnnotationMetaDataProvider (org.hibernate.validator.internal.metadata.provider)
getBeanConfigurationForHierarchy:234, BeanMetaDataManagerImpl (org.hibernate.validator.internal.metadata)
createBeanMetaData:201, BeanMetaDataManagerImpl (org.hibernate.validator.internal.metadata)
getBeanMetaData:165, BeanMetaDataManagerImpl (org.hibernate.validator.internal.metadata)
validateParameters:267, ValidatorImpl (org.hibernate.validator.internal.engine)
validateParameters:235, ValidatorImpl (org.hibernate.validator.internal.engine)
invokeValidatorForArguments:261, MethodValidationAdapter (org.springframework.validation.beanvalidation)
validateArguments:241, MethodValidationAdapter (org.springframework.validation.beanvalidation)
validateArguments:115, HandlerMethodValidator (org.springframework.web.method.annotation)
applyArgumentValidation:83, HandlerMethodValidator (org.springframework.web.method.annotation)
invokeForRequest:188, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:118, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:920, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:830, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1089, DispatcherServlet (org.springframework.web.servlet)
doService:979, DispatcherServlet (org.springframework.web.servlet)
processRequest:1014, FrameworkServlet (org.springframework.web.servlet)
doPost:914, FrameworkServlet (org.springframework.web.servlet)
service:547, HttpServlet (jakarta.servlet.http)
service:885, FrameworkServlet (org.springframework.web.servlet)
service:72, TestDispatcherServlet (org.springframework.test.web.servlet)
service:614, HttpServlet (jakarta.servlet.http)
...
The used Hibernate-Validator version was for both Spring versions the same: hibernate-validator:8.0.1.Final
Thus, I assume the issue is on Spring Boot's side, not Hibernate.
Comment From: sdeleuze
I can reproduce the same error with Spring Boot 3.1 when adding a @Validated
annotation on the controller, so this error looks like a side effect of Spring Framework 6.1 enabling builtin validation by default combined with something that Hibernate Validator does not support as configured here.
It looks like type argument constraints require an Hibernate Validator value extractor to be configured.
@pfuerholz Could you try to configure such ResponseEntity
value extractor and see if that workarounds this error?
@jhoeller @rstoyanchev Any thoughts on if we should update our default arrangement now that such validation is enabled automatically (like registering value extractors for common wrappers like ResponseEntity
)? Also is there a way to opt-out to allow to skip the validation of the returned value here?
Comment From: rstoyanchev
Until Spring Boot 3.1 (3.1.8 to be exact) following endpoint was fine:
@GetMapping("/foo/bar/{id}")
@NotNull ResponseEntity<@NotNull String> getById(@PathVariable @NotNull String id) {}
Was the @NotNull
on the String body enforced though? The fact the error did not appear before means that validation likely was not applied. Hibernate-validator has no way of knowing how to access the String value, and this is what a ValueExtractor
registration is meant to help with.
Before Spring Framework 6.1, method validation (including return value validation) would have been applied through a validation proxy if your controller has a class-level @Validated
. I suspect it doesn't, or otherwise it should have been failing just the same.
Comment From: rstoyanchev
@sdeleuze yes I think we could register a ValueExtractor
for ResponseEntity
. In terms of opting out, if the intent is to always opt out, is there a reason to have the validation annotations at all? Anything else implies opting out in sometimes, but not always, and I'm not sure what the case is for that.
Comment From: sdeleuze
Good point, if there are validation annotations (unlike JSR 305 or JSpecify ones), that communicates an intent to perform the validation at runtime indeed. So let's maybe wait @pfuerholz feedback to see if the registration of a proper ValueExtractor
works as expected (please share the repro as an attache archive or a link to a repository). We will then decide if that's the user responsibility or if Spring should do it out of the box.
Comment From: pfuerholz
I was unsuccessful by adding a ValueExtractor
, the error is still the same.
Here is what I did:
public class NotNullAnnotationValueExtractor implements ValueExtractor<@ExtractedValue @NotNull String> {
@Override
public void extractValues(String originalValue, ValueReceiver receiver) {
receiver.value("non null", Objects.requireNonNull(originalValue));
}
}
and registered the additional NotNullAnnotationValueExtractor
:
@Component
public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
super.postProcessConfiguration(configuration);
configuration.addValueExtractor(new NotNullAnnotationValueExtractor());
}
}
The additional ValueExtractor
is not get called. My assumption this solution does not work is since the @NotNull
-annotation does not go into a method signature (not distinguishable from variant without @NotNull
).
Comment From: sdeleuze
If this use case is not supported by Hibernate Validator, and since the original confusion came from the fact in Spring Framework 6.0 @Validated
was required to ensure proper validation, I would suggest to decline this issue as I don't see any actionable item. @pfuerholz Ok from your point of view?
Comment From: quaff
I was unsuccessful by adding a
ValueExtractor
, the error is still the same. Here is what I did:
public class NotNullAnnotationValueExtractor implements ValueExtractor<@ExtractedValue @NotNull String> { @Override public void extractValues(String originalValue, ValueReceiver receiver) { receiver.value("non null", Objects.requireNonNull(originalValue)); } }
and registered the additional
NotNullAnnotationValueExtractor
:``` @Component public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
@Override protected void postProcessConfiguration(Configuration<?> configuration) { super.postProcessConfiguration(configuration); configuration.addValueExtractor(new NotNullAnnotationValueExtractor()); }
} ```
The additional
ValueExtractor
is not get called. My assumption this solution does not work is since the@NotNull
-annotation does not go into a method signature (not distinguishable from variant without@NotNull
).
@pfuerholz You should write ValueExtractor
for ResponseEntity
like this:
import jakarta.validation.valueextraction.ExtractedValue;
import jakarta.validation.valueextraction.ValueExtractor;
import org.springframework.http.ResponseEntity;
class ResponseEntityValueExtractor implements ValueExtractor<ResponseEntity<@ExtractedValue ?>> {
@Override
public void extractValues(ResponseEntity<?> originalValue, ValueReceiver valueReceiver) {
valueReceiver.value(null, originalValue.getBody());
}
}
Comment From: sdeleuze
@pfuerholz Please let us know how it goes with @quaff proposal.
Comment From: quaff
@sdeleuze I confirm it works as:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Configuration;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.valueextraction.ExtractedValue;
import jakarta.validation.valueextraction.ValueExtractor;
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/foo/bar/{id}")
@NotNull
ResponseEntity<@NotNull String> getById(@PathVariable @NotNull String id) {
return ResponseEntity.ofNullable(null);
}
@Bean
static LocalValidatorFactoryBean localValidatorFactoryBean() {
return new LocalValidatorFactoryBean() {
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
configuration.addValueExtractor(new ResponseEntityValueExtractor());
super.postProcessConfiguration(configuration);
}
};
}
static class ResponseEntityValueExtractor implements ValueExtractor<ResponseEntity<@ExtractedValue ?>> {
@Override
public void extractValues(ResponseEntity<?> originalValue, ValueReceiver valueReceiver) {
valueReceiver.value(null, originalValue.getBody());
}
}
}
Should we register ResponseEntityValueExtractor
as built-in?
Comment From: pfuerholz
Thanks @sdeleuze, that helped - at least in outermost cases.
What I do not understand is, why ResponseValueExtractor
works: If I set a breakpoint into extractValues
the code is not run through!
And: Following case still produces the known error:
@PatchMapping("/foo/bar/{id}")
@NotNull ResponseEntity<@NotNull Void> doSomething(@PathVariable @NotNull String id) {
...
Though this is a rather specific case (@NotNull Void
) it is not understandable why it behaves differently.
Comment From: rstoyanchev
@pfuerholz the ValueExtractor you wrote in https://github.com/spring-projects/spring-framework/issues/32375#issuecomment-1986875406 is not for a ResponseEntity. Meanwhile @quaff provided example snippets, but your latest comment does not acknowledge any of that, so it's unclear what you're actually trying.
This is all expected to work. If you are still having issues, please provide a sample we can run.
Comment From: pfuerholz
@rstoyanchev, yes, @quaff's solution works, I stated this in my last response (initially thought it was @sdeleuze's merit...).
Nevertheless there is another issue, though not comparably important. Following return value can not be processed:
@NotNull ResponseEntity<@NotNull Void>
After all it is rather strange to me why the ResponseEntityValueExtractor
in general does help although it is not run through and why it does not work for Void
...
Comment From: sdeleuze
This discussion has shown that Spring behaves as expected here, the original trigger for this issue being some misunderstanding of how the validation is triggered in Spring Framework 6.1 (enabled by default) and previously (@Validated
required).
Annotating a generic type parameter with @NotNul
or similar validation annotation clearly indicates the intent of performing some validation here, and such validation should fulfill Hibernate Validator requirements, here to create and configure a related ValueExtractor
. That is exactly what somebody using @Validated
would have to do previously, and there is little sense to use @NotNull
without @Validated
in previous Spring Framework versions.
As a consequence, we prefer to decline the enhancement request that would consist of registering automatically a ResponseEntityValueExtractor
for now, and we prefer to wait if this is a popular demand with multiple similar asks before considering adding it as it is just a convenience that any developer can configured easily.
Comment From: pfuerholz
Thanks @sdeleuze for your description. At least now I think I understand it better:
My intention for ResponseEntity<@NotNull String>
is to declare that the response will never be empty. What it means in runtime context is, that the response must be able to be extracted and checked if it is really not empty. For this a matching ValueExtractor
must be provided.
Comment From: nilshartmann
Hi @rstoyanchev , @quaff ,
I tried your code example from https://github.com/spring-projects/spring-framework/issues/32375#issuecomment-1987965898 and indeed the error "No value extractor found for type parameter 'T' of type ..." is gone but it seems the ResponseEntity
's body is not validated - no matter if I add @Validate
anywhere or not.
@RestController
public class PingController {
record PingResponse(@NotNull String status) {
}
// no validation error
@GetMapping("/ping")
ResponseEntity<PingResponse> getPing() {
return ResponseEntity.ok(new PingResponse(null));
}
// also no validation error, but without ResponseEntityValueExtractor, it gives the
// "No value extractor" error
@GetMapping("/ping")
ResponseEntity<@Valid PingResponse> getPing2() {
return ResponseEntity.ok(new PingResponse(null));
}
}
I also tried ResponseEntity<@NotNull PingResponse>
and returned a null
body in ResponseEntity
, but also no validation error occured. I also set a breakpoint in ResponseEntityValueExtractor.extractValues
and it is never hit.
Btw. constraint annotations at method arguments are correctly validated on the controller class.
Any ideas, what I'm doing wrong? (Spring Boot 3.4.0-M1 with included Spring 6.2.0-M6)
Thanks a lot!