Keywords: Hibernate Validator | Cross-Field Validation | JSR-303
Abstract: This article provides an in-depth exploration of two primary methods for implementing cross-field validation in Hibernate Validator 4.x. It details the class-level annotation approach using FieldMatch validators, covering custom annotation definition, validator implementation, and practical application in form validation. Additionally, it presents the simplified @AssertTrue annotation method as an alternative. Through comparative analysis of both approaches' strengths and limitations, the article offers guidance for developers in selecting appropriate solutions for different scenarios, emphasizing adherence to JSR-303 specifications.
The Need for Cross-Field Validation
In practical Java Bean Validation (JSR-303) applications, developers frequently encounter scenarios requiring validation of relationships between multiple fields. Typical examples include password confirmation, email address matching, date range checking, and similar interdependent validations that cannot be satisfied by individual field validators.
Standard Implementation with Class-Level Annotations
Following JSR-303 best practices, cross-field validation should be implemented at the class level rather than having one field's annotation validate another. This approach maintains clarity and maintainability of validation logic.
Custom Annotation Definition
First, define a class-level annotation @FieldMatch:
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch {
String message() default "{constraints.fieldmatch}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String first();
String second();
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
FieldMatch[] value();
}
}
Validator Implementation
The validator class must implement the ConstraintValidator<FieldMatch, Object> interface:
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
private String firstFieldName;
private String secondFieldName;
@Override
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
}
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
try {
final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
} catch (final Exception ignore) {
return true;
}
}
}
Practical Application Example
Application in user registration forms:
@FieldMatch.List({
@FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
@FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationForm {
@NotNull
@Size(min=8, max=25)
private String password;
@NotNull
@Size(min=8, max=25)
private String confirmPassword;
@NotNull
@Email
private String email;
@NotNull
@Email
private String confirmEmail;
}
Simplified Implementation Alternative
As a supplementary approach, the @AssertTrue annotation provides a simpler method for cross-field validation:
public class MyBean {
@Size(min=6, max=50)
private String pass;
private String passVerify;
@AssertTrue(message = "Fields `pass` and `passVerify` should be equal")
private boolean isValidPass() {
return Objects.equals(pass, passVerify);
}
}
Comparative Analysis and Selection Guidance
Both approaches have distinct advantages and limitations:
- Class-Level Annotation Approach: Complies with JSR-303 specifications, supports multiple validation pairs, allows customizable error messages, but requires more complex implementation
- @AssertTrue Approach: Simple implementation with no additional dependencies, but validation logic is scattered across multiple methods and less reusable
Recommend using the class-level annotation approach when validation logic needs reuse or multiple field pairs require validation, while the @AssertTrue approach is suitable for simpler scenarios.