-
Notifications
You must be signed in to change notification settings - Fork 38.6k
Description
Affects: 5.3.22
Related issue: #16855.
This issue is basically the same as #16855, but for a custom wrapper (#16855 only handles Optional
).
SpringValidatorAdapter
couldn't process ConstraintViolations because it can't traverse property path. It throws this exception:
Caused by: java.lang.IllegalStateException: JSR-303 validated property 'location.name' does not have a corresponding accessor for Spring data binding - check your DataBinder's configuration (bean property versus direct field access)
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.processConstraintViolations(SpringValidatorAdapter.java:188) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:109) ~[spring-context-5.3.22.jar:5.3.22]
at com.example.demo.DemoApplication.lambda$run$0(DemoApplication.java:29) ~[classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:762) ~[spring-boot-2.7.3.jar:2.7.3]
... 5 common frames omitted
Caused by: org.springframework.beans.NotReadablePropertyException: Invalid property 'location.name' of bean class [com.example.demo.UpdateRequest]: Bean property 'location.name' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:627) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:617) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.validation.AbstractPropertyBindingResult.getActualFieldValue(AbstractPropertyBindingResult.java:104) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.validation.AbstractBindingResult.getRawFieldValue(AbstractBindingResult.java:284) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.getRejectedValue(SpringValidatorAdapter.java:318) ~[spring-context-5.3.22.jar:5.3.22]
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.processConstraintViolations(SpringValidatorAdapter.java:174) ~[spring-context-5.3.22.jar:5.3.22]
... 8 common frames omitted
But the problem in this case not with getter/setter, but with the fact that actual field type is not simple POJO, but a custom wrapper class.
Example:
public class UpdateRequest {
// Note the custom wrapper type
@NotNull
@Valid
private NullableOptional<Location> location = NullableOptional.absent();
public NullableOptional<Location> getLocation() {
return location;
}
public void setLocation(NullableOptional<Location> location) {
this.location = location;
}
}
public class Location {
@NotEmpty
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public final class NullableOptional<T> {
private static final NullableOptional<?> ABSENT = new NullableOptional<>(true, null);
private final boolean absent;
private final T value;
private NullableOptional(boolean absent, T value) {
this.absent = absent;
this.value = !absent ? value : null;
}
public static <T> NullableOptional<T> absent() {
@SuppressWarnings("unchecked")
NullableOptional<T> t = (NullableOptional<T>) ABSENT;
return t;
}
public static <T> NullableOptional<T> of(T value) {
return new NullableOptional<>(false, value);
}
public T get() {
if (absent) {
throw new NoSuchElementException("No value present");
}
return value;
}
public boolean isPresent() {
return !absent;
}
public boolean isAbsent() {
return absent;
}
public void ifPresent(Consumer<? super T> action) {
if (!absent) {
action.accept(value);
}
}
public void ifPresentOrElse(Consumer<? super T> action, Runnable absentAction) {
if (!absent) {
action.accept(value);
} else {
absentAction.run();
}
}
public T orElse(T other) {
return !absent ? value : other;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return obj instanceof NullableOptional<?> other
&& absent == other.absent
&& Objects.equals(value, other.value);
}
@Override
public int hashCode() {
return Objects.hash(absent, value);
}
@Override
public String toString() {
return !absent
? String.format("NullableOptional[%s]", value)
: "NullableOptional.absent";
}
}
@UnwrapByDefault
public class NullableOptionalValueExtractor implements ValueExtractor<NullableOptional<@ExtractedValue ?>> {
@Override
public void extractValues(NullableOptional<?> originalValue, ValueReceiver receiver) {
if (originalValue.isPresent()) {
receiver.value(null, originalValue.get());
}
}
}
Note, that NullableOptionalValueExtractor
is registered using META-INF/services/javax.validation.valueextraction.ValueExtractor
as described here and correctly unwraps the field value for validation purposes. @UnwrapByDefault
makes validation annotations (e.g. @NotNull
) on NullableOptional
field work as if they were on a wrapped type (Location
in this example).
And the validation example, that throws mentioned exception:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
ApplicationRunner run(Validator validator) {
return args -> {
Location location = new Location();
location.setName(null);
UpdateRequest updateRequest = new UpdateRequest();
updateRequest.setLocation(NullableOptional.of(location));
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(updateRequest, "updateRequest");
validator.validate(updateRequest, bindingResult);
System.out.println("bindingResult: " + bindingResult);
};
}
}
The validation itself that Hibernate-validator performs works fine. But then SpringValidatorAdapter
throws. Looks like it isn't aware of ValueExtractor
concept.
What can be done in this case to make it work (or at least not throw)? Can the support of custom wrapper types be added in some generic way that will work out of the box (just like it's done in Hibernate validator thanks to ValueExtractor
)? Or, at least, skip the extraction of a value if not possible instead of throw the error.
What workaround exists for now? I tried to debug the source code and unfortunately didn't found any possibility for a workaround.
The main goal is to use NullableOptional<T>
(or any other custom wrapper) for fields of JSON body of PATCH
requests, where a field value could be either null, non-null or absent (when field is absent in JSON representation altogether) and business logic needs to distinguish between all 3 cases. Sometimes people use Optional<T>
for this purpose, but it's error-prone and ugly, as third state of Optional
field is the null-reference, that is anti-pattern and usually unexpected usage of Optional
type. Custom wrapper type solves these problems as it's intended to be used specifically for the mentioned case and will never reference to null
(but could contain null
inside).
Minimal Demo Project: custom-container-validation-bug.zip