I'm having a problem where, when I have multiple nested beans of the same type, the returned message may end up being the same, which can confuse the user:
Minimal example (the real beans have lots of fields):
class A {
@NotBlank("Name is obligatory.")
String name;
@NotBlank("Address is obligatory.")
String name;
}
class B {
@Valid
A origin;
@Valid
A destination;
}
If I run B
against the validator with blank names, it will always return "Name is obligatory.", no matter if it comes from the origin
or from the destination
. I know that the error message comes with the field names, but that information, by itself, is not very useful for the end user.
Is there some annotation that validates the nested beans similarly to what @Valid
does, but adding a prefix, so that instead of saying "Name is obligatory.", it would say either "Original person: Name is obligatory." or "Destination person: Name is obligatory."?
I couldn't find any out-of-the box way to implement what was needed, so I had to implement a custom constraint:
PrefixConstraint.java
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Documented
@Constraint(validatedBy = PrefixConstraintValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface PrefixConstraint {
String message() default "Prefix missing";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
PrefixConstraintValidator.java
import java.util.Set;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
public class PrefixConstraintValidator implements ConstraintValidator<PrefixConstraint, Object> {
private PrefixConstraint constraint;
@Override
public void initialize(PrefixConstraint constraintAnnotation) {
this.constraint = constraintAnnotation;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> violations = validator.validate(value, this.constraint.groups());
context.disableDefaultConstraintViolation();
for (ConstraintViolation<Object> violation : violations) {
context
.buildConstraintViolationWithTemplate(this.constraint.message() + ": " + violation.getMessage())
.addPropertyNode(violation.getPropertyPath().toString())
.addConstraintViolation();
}
return violations.isEmpty();
}
}
Usage example:
class A {
@NotBlank("Name is obligatory.")
String name;
@NotBlank("Address is obligatory.")
String name;
}
class B {
@PrefixConstraint(message = "Original person")
A origin;
@PrefixConstraint(message = "Destination person")
A destination;
}
Now, if the names are left blank, the validator will return the message: "Original person: Name is obligatory" and "Destination person: Name is obligatory".