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(); } }
Could you please post the value of “bad.value” from your message bundle? Great post, thanks!
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!