0

I have some trouble using cross field validation in Spring Boot. For example there is a class with four fields. The first field is mandatory, all others are optional, but at least one of optional fields must exist.

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
}

Now I'm looking for a dynamic solution because I need to extend the class easily. How can I do it?

Kai P
  • 1

1 Answers1

0

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
}
Kai P
  • 1