This commit introduces of
method in Validator
to provide a way to create a validator for the specific type <T>
using BiConsumer<T, Errors>
and define the validator in a functional way.
Validator passwordEqualsValidator = Validator.of(PasswordResetForm.class)
.apply((form, errors) -> {
if (!Objects.equals(form.getPassword(), form.getConfirmPassword())) {
errors.rejectValue("confirmPassword",
"PasswordEqualsValidator.passwordResetForm.password",
"password and confirm password must be same.");
}
});
This also eliminates the boilerplate for implementing the supports
method.
BONUS: Supporting BiConsumer allows us to magically adapt 3rd party Validators (like YAVI) that return BiConsumer. (I'm the creator of YAVI.)
(original proposal)
Validator validator = Validator.of(CartItem.class)
.apply(ValidatorBuilder.<CartItem>of()
.constraint(CartItem::itemId, "itemId", c -> c.notEmpty())
.constraint(CartItem::amount, "amount", c -> c.greaterThanOrEqual(0))
.constraint(CartItem::price, "price", c -> c.greaterThanOrEqual(0))
.build(Errors::rejectValue));
(revised version)
Validator validator = Validator.forInstanceOf(CartItem.class, ValidatorBuilder.<CartItem>of()
.constraint(CartItem::itemId, "itemId", c -> c.notEmpty())
.constraint(CartItem::amount, "amount", c -> c.greaterThanOrEqual(0))
.constraint(CartItem::price, "price", c -> c.greaterThanOrEqual(0))
.build(Errors::rejectValue));
Comment From: poutsma
A nice proposal to modernise Validator
, thanks @making!
I do find the usage of Function
(and the resulting apply
) a bit complicated, and I don't believe it's necessary. Instead, you can have the BiConsumer
as a second parameter for the of
method, like so:
static <T> Validator of(Class<T> targetClass, BiConsumer<T, Errors> validator) {
return new Validator() {
@Override
public boolean supports(Class<?> clazz) {
return targetClass.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
validator.accept(targetClass.cast(target), errors);
}
};
}
Comment From: poutsma
@making, We would like to make this part of Spring Framework 6.1. Are you ok with changing the PR to reflect our suggestions, or would you like me to pick it up from here?
Comment From: sdeleuze
+1 for such functional approach in Validator
.
Comment From: making
@poutsma Updated the pr!
Comment From: sbrannen
Instead, you can have the
BiConsumer
as a second parameter for theof
method
That's a big improvement. 👍
Comment From: poutsma
@poutsma Updated the pr!
Looking good!
A couple of improvements that our team discussed before it can be merged in 6.1.
- It would be nice to have a non-opinionated variant of the factory method that takes just a predicate and biconsumer, something like:
static <T> Validator of(Predicate<Class<?>> supports, BiConsumer<T, Errors> delegate)
This allows users to have their own supports
logic in a predicate.
- I think it's cleaner to move the anonymous Validator
implementation into its own package-protected class named DefaultValidator
(or something similar). This helps keep the Validator
interface cleaner, and also improves the readability of any potential stacktrace.
- Sometimes you want to specify an exact class match, instead of allowing all subclasses. So instead of using targetClass.isAssignableFrom
, you'd use targetClass.equals
. @rstoyanchev suggested to have two separate methods:
- forInstanceOf(Class<T>, BiConsumer<T, Errors>)
(using isAssignableFrom
, similar to the current PR), and
- forType(Class<T>, BiConsumer<T, Errors>)
(using equals
).
Note that forInstanceOf
and forType
can be implemented using the predicate-based of
method suggested above (i.e. of(targetClass::isAssignableFrom, delegate)
and of(targetClass::equals, delegate)
).
Let me know what you think about these suggestions.
Comment From: making
@poutsma
I'm in favor of having both forInstanceOf
and forType
, but I'm not sure it makes sense to have the of(Predicate<Class<?>> supports, BiConsumer<T, Errors> delegate)
method.
Since the validate
methods accepts any object, to delegate validation logic to a BiConsumer<T, Errors>
, the supports
method must ensure that the object is instance of the T
class.
I think it would make more sense to have a class like bellow, what do you think?
class TypedValidator<T> implements Validator {
private final Class<T> targetClass;
private final BiPredicate<Class<T>, Class<?>> typeVerifier;
private final BiConsumer<T, Errors> delegate;
public TypedValidator(Class<T> targetClass, BiPredicate<Class<T>, Class<?>> classVerifier, BiConsumer<T, Errors> delegate) {
this.targetClass = targetClass;
this.typeVerifier = classVerifier;
this.delegate = delegate;
}
@Override
public boolean supports(Class<?> clazz) {
return this.typeVerifier.test(this.targetClass, clazz);
}
@Override
public void validate(Object target, Errors errors) {
this.delegate.accept(this.targetClass.cast(target), errors);
}
}
and factory methods in Validator
static <T> Validator forInstanceOf(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
return new TypedValidator<>(targetClass, Class::isAssignableFrom, delegate);
}
static <T> Validator forType(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
return new TypedValidator<>(targetClass, Objects::equals, delegate);
}
Comment From: poutsma
@making Sorry for not responding earlier.
I'm liking that TypedValidator
, but if I find the BiPredicate<Class<T>, Class<?>>
a bit confusing. I think it would be simpler and clearer to have a Predicate<Class<?>>
instead, and in forInstanceOf
and forType
use the targetClass
parameter instead of the field.
Comment From: making
@poutsma Sorry for the late reply. Did you mean
package org.springframework.validation;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
class TypedValidator<T> implements Validator {
private final Class<T> targetClass;
private final Predicate<Class<?>> typeVerifier;
private final BiConsumer<T, Errors> delegate;
public TypedValidator(Class<T> targetClass, Predicate<Class<?>> typeVerifier, BiConsumer<T, Errors> delegate) {
this.targetClass = targetClass;
this.typeVerifier = typeVerifier;
this.delegate = delegate;
}
@Override
public boolean supports(Class<?> clazz) {
return this.typeVerifier.test(clazz);
}
@Override
public void validate(Object target, Errors errors) {
this.delegate.accept(this.targetClass.cast(target), errors);
}
}
and
static <T> Validator forType(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
return new TypedValidator<>(targetClass, clazz -> Objects.equals(targetClass, clazz), delegate);
}
static <T> Validator forInstanceOf(Class<T> targetClass, BiConsumer<T, Errors> delegate) {
return new TypedValidator<>(targetClass, targetClass::isAssignableFrom, delegate);
}
?
It looks as same as proposed in https://github.com/spring-projects/spring-framework/pull/29890#issuecomment-1459983685
Maybe DefaultValidator
is better as there is no longer guarantee that the input object is instance of the T
class from TypedValidator
perspective
Comment From: poutsma
Thank you, @making, for submitting this PR. It has now been merged , see dd9f03d9b9c858d44bfc4b64be06e064ce9a0334.
I made some changes, including the TypedValidator
that was mentioned above. Hopefully it is now clear what I meant.
Feel free to comment or create subsequent PRs if you have any further suggestions regarding this piece of functionality.