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!