How to conditionally call spring validators
I have a model as shown below. The existing code already has individual property checks. Now I have a requirement to ignore the existing checks on billing_country and address if buyer.method
is foo
. I thought I could have a custom validator at the level buyer
, validate the method, and only call validations when buyer.method!=foo
. Is this the correct approach? Are there any better alternatives?
"buyer": {
"method": "foo",
"instruments": [
{
"card": {
"type": "MASTERCARD",
"number": "234234234234234",
"expire_month": "12",
"expire_year": "2017",
"billing_country" : "US"
"Address" : {
"line1": "summer st",
"city": "Boston"
"country": "US"
}
}
}
]
}
source to share
There are two ways to do this.
You can either
-
create a custom validation annotation and validator that validates the value
method
and then applies the appropriate validation to other fields -
use validation groups where you manually validate the value
method
, then choose which group to use; your annotated fields then only need to be changed to apply when this group is active.
Option number 1
Define annotation at class level:
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { MethodAndInstrumentsValidator.class })
@Documented
public @interface ValidMethodAndInstruments {
String message() default "{my.package.MethodAndInstruments.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Define a validator:
public class MethodAndInstrumentsValidator
implements ConstraintValidator<ValidMethodAndInstruments, Buyer> {
public final void initialize(final ValidMethodAndInstruments annotation) {
// setup
}
public final boolean isValid(final Buyer value,
final ConstraintValidatorContext context) {
// validation logic
if(!"foo".equals(value.getMethod())) {
// do whatever conditional validation you want
}
}
}
Annotate customer class:
@ValidMethodAndInstruments
public class Buyer { }
Option number 2
Here you will need to manually check.
First, define the validation groups:
public interface FooValidation {}
public interface NotFooValidation {}
Configure the validator:
@Bean
public LocalValidatorFactoryBean validatorFactory() {
return new LocalValidatorFactoryBean();
}
In the controller (?) Check the value method
and do the check:
@Autowired
private Validator validator;
// ...
public void validate(Object a, Class<?>... groups) {
Set<ConstraintViolation<Object>> violations = validator.validate(a, groups);
if(!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
// ...
validate(buyer, "foo".equals(buyer.getMethod())
? FooValidation.class
: NotFooValidation.class);
Finally, change the groups in the model / dto class:
public class Buyer {
// ...
@Null(groups = FooValidation.class)
@NotNull(groups = NotFooValidation.class)
protected String billingCountry;
}
source to share
@beerbajay helped to some extent, so I'm going to accept his answer. However, I also wanted to write down a few of the problems we faced. We could not use the summary @Null
and @NotNull
the address, as it may be zero. Second, since we had to wrap this in a custom constraint validator, so NotFooValidation.class
we got an extra validation message line for that, which was not desirable. Finally, we were unable to use the groups @Valid
that were already present at Address
. We tried it @Validated
, but for some reason it didn't work. We completed the implementation ReaderInterceptor
to intercept the request and manually invoked the checks that were needed. Hope this helps someone along the way.
source to share