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());