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!