Using Ishikawa Yoshi's hint, I found the solution myself. Here is my implementation for all who are interested.
First I created a new annotation
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {AtLeastOneOfValidator.class})
public @interface AtLeastOneOf {
String message() default "{one.of.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] fields();
int max() default 2147483647;
}
And then the related validator
public class AtLeastOneOfValidator implements ConstraintValidator<AtLeastOneOf, Object> {
private String[] fields;
private int max;
@Override
public void initialize(AtLeastOneOf annotation) {
this.fields = annotation.fields();
this.max = annotation.max();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);
int matches = countNumberOfMatches(wrapper);
if (matches > this.max) {
setValidationErrorMessage(context, "one.of.too.many.matches.message");
return false;
} else if (matches == 0) {
setValidationErrorMessage(context, "one.of.no.matches.message");
return false;
}
return true;
}
private int countNumberOfMatches(BeanWrapper wrapper) {
int matches = 0;
for (String field : this.fields) {
Object value = wrapper.getPropertyValue(field);
boolean isPresent = detectOptionalValue(value);
if (value != null && isPresent) {
matches++;
}
}
return matches;
}
@SuppressWarnings("rawtypes")
private boolean detectOptionalValue(Object value) {
if (value instanceof Optional) {
return ((Optional) value).isPresent();
}
return true;
}
private void setValidationErrorMessage(ConstraintValidatorContext context, String template) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("{" + template + "}").addConstraintViolation();
}
}
Now the annotation can be used
@AtLeastOneOf(fields = {"clients", "items", "orders"})
public class DataContainer {
@NotNull
private String provider;
@Valid
private List<Client> clients;
@Valid
private List<Item> items;
@Valid
private List<Order> orders;
// Getter and setter omitted for simplicity
}