In one of my projects I have a custom Spring Validator that validates a nested object structure, and adds per-field error messages. As an example, a field nested inside an array might produce an error like the following:

  • array[0].field must be a valid value

The Errors object works as a stack, so field names have to be pushed as the validator iterates through arrays and nested objects. So, a very simple validator might look like this:

public class MyValidator implements Validator {
  public void validate(Object o, Errors errors) {
    MyObject m = (MyObject) o;
    for (int i = 0; i < m.getArray().size(); i++) {
      errors.pushNestedPath(format("{0}[{1}]","array",i));
      if (isBad(m.getArray().get(i).getField())) {
        errors.rejectValue("field", "bad.value");
      }
      errors.popNestedPath();
    }
    for (int i = 0; i < m.getArray2().size(); i++) {
      errors.pushNestedPath(format("{0}[{1}]","array2",i));
      if (isBad(m.getArray2().get(i).getField())) {
        errors.rejectValue("field", "bad.value");
      }
      errors.popNestedPath();
    }
 //Repeat 3 times for different collections
  }
}

 

This looks bad because I needed a loop counter (i), in addition to the value of the field from the array, in order  to properly format the error message. This pattern was also repeated 4 different times for different collections, resulting in a lot of repetitive code. After some refactoring, I was able to use Consumer to cut it down to this:

public class MyValidator implements Validator {

  @Override
  public void validate(Object o, Errors errors) {
    MyObject m = (MyObject) o;
    validateCollection(errors, "array", m.getArray(), item -> commonCheck(errors,"field",item.getField()));
    validateCollection(errors, "array2", m.getArray2(), item -> commonCheck(errors,"field",item.getField()));
    //validateCollection(...)
    //validateCollection(...)
  }
  static void commonCheck(Errors errors, String fieldName, String fieldValue) {
    if (isBad(fieldValue)) {
      errors.rejectValue(fieldName, "bad.value");
    }
  }
  public static <T> void validateCollection(Errors errors, String fieldName, Collection<T> items, Consumer<T> validationFunction) {
    AtomicInteger ai = new AtomicInteger();
    items
        .stream()
        .forEachOrdered(item ->
            validateNested(errors, format("{0}[{1}]", fieldName, ai.getAndIncrement()), validationFunction, item)
        );
  }

  public static <T> void validateNested(Errors errors, String path, Consumer<T> validator, T item) {
    errors.pushNestedPath(path);
    validator.accept(item);
    errors.popNestedPath();
  }
}