Using Consumer in Spring Validator to validate nested collections

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

2 thoughts on “Using Consumer in Spring Validator to validate nested collections”

  1. Could you please post the value of “bad.value” from your message bundle? Great post, thanks!

  2. Great post!
    I think this is really useful as validation of nested spring objects can be very tricky sometimes and this is really an elegant solution to this problem!

Leave a Reply