If your validation depends on multiple fields (eg. isPrimary
and either primaryDTO
or secondaryDTO
), then the only solution is to write a custom validator on classlevel (UserDTO
) which will implement the conditional validation itself.
For example, create an annotation:
@Documented
@Retention(RUNTIME)
@Target({ANNOTATION_TYPE, TYPE})
@Constraint(validatedBy = SecondaryValidator.class)
public @interface ValidSecondary {
String message() default "Invalid secondary";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And create a validator that only validates the secondaryDTO
field when isPrimary()
is false
:
@Component
public class SecondaryValidator implements ConstraintValidator<ValidSecondary, UserDTO> {
private Validator validator;
public SecondaryValidator(Validator validator) {
this.validator = validator;
}
@Override
public boolean isValid(UserDTO userDTO, ConstraintValidatorContext constraintValidatorContext) {
if (userDTO.isPrimary()) {
return true;
} else {
return validator.validate(userDTO.getSecondaryDTO()).isEmpty();
}
}
}
After that, you can remove the @Valid
annotation from the secondaryDTO
field and add the @ValidSecondary
annotation on top of your UserDTO
:
@ValidSecondary // Add this
public class UserDTO {
@Valid
private PrimaryDTO primaryDTO;
private SecondaryDTO secondaryDTO; // No more @Valid
private boolean primary;
}
However, in this case you'll lose any constraint violation message from within the SecondaryDTO
, if you want to have some kind of passing through mechanism, you can add the violations to the constraintValidatorContext
within the isValid()
method, for example:
Set<ConstraintViolation<SecondaryDTO>> violations = validator.validate(userDTO.getSecondaryDTO());
violations.forEach(violation -> constraintValidatorContext
.buildConstraintViolationWithTemplate(violation.getMessage())
.addConstraintViolation());