One question I'm being asked pretty often when talking about the Bean Validation API (JSR 303) is, how cross-field validation can be done. The canonical example for this is a class representing a calendar event, where the end date of an event shall always be later than the start date.
This and other scenarios where validation depends on the values of multiple attributes of a given object can be realized by implementing a class-level constraint.
That basically works pretty well, nevertheless this is one part of the BV spec, I'm not too comfortable with. This is mainly for two reasons:
- You'll probably need a dedicated constraint for every reasonably complex business object. Providing an annotation, a validator implementation and an error message for each can become pretty tedious.
- Business objects typically know best themselves, whether they are in a consistent, valid state or not. By putting validation logic into a separate constraint validator class, objects have to expose their internal state, which otherwise might not be required.
To circumvent these problems, I suggested a while ago a generic constraint annotation, which allows the use of script expressions written in languages such as Groovy to implement validation logic directly at the validated class.
This approach frees you from the need to implement dedicated constraints for each relevant business object, but also comes at the cost of losing type-safety at compile-time.
The constraint presented in the following therefore tries to combine genericity with compile-time type safety. The basic idea is to implement validation logic within the business objects themselves and to invoke this logic from within a generic constraint class.
In order to do so let's first define an interface to be implemented by any validatable class:
1 2 3 4 |
public interface Validatable { public boolean isValid(); } |
Next we define a constraint annotation, @SelfValidating, which we'll use later on to annotate Validatable implementations:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Target( { TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = SelfValidatingValidator.class) @Documented public @interface SelfValidating { String message() default "{de.gmorling.moapa.self_validating.SelfValidating.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
Of course we also need a constraint validator implementation, which is able to evaluate that constraint:
1 2 3 4 5 6 7 8 9 10 11 |
public class SelfValidatingValidator implements ConstraintValidator<SelfValidating, Validatable> { public void initialize(SelfValidating constraintAnnotation) {} public boolean isValid(Validatable value, ConstraintValidatorContext constraintValidatorContext) { return value.isValid(); } } |
The implementation is trivial, as nothing is to do in initialize() and the isValid() method just delegates the call to the validatable object itself.
Finally we need a properties file named ValidationMessages.properties containing the default error message for the constraint:
1 |
de.gmorling.moapa.self_validating.SelfValidating.message=Validatable object couldn't be validated successfully. |
Taking the calendar event example from the beginning, usage of the constraint might look as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@SelfValidating public class CalendarEvent implements Validatable { @NotNull private final String title; private final Date startDate; private final Date endDate; public CalendarEvent(String title, Date startDate, Date endDate) { this.title = title; this.startDate = startDate; this.endDate = endDate; } public boolean isValid() { return startDate == null || endDate == null || startDate.before(endDate); } @Override public String toString() { DateFormat format = new SimpleDateFormat("dd.MM.yyyy"); return title + " from " + (startDate == null ? "-" : format.format(startDate)) + " till " + (endDate == null ? "-" : format.format(endDate)); } } |
A short unit test shows that the constraint works as expected:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public class SelfValidatingTest { private static Validator validator; private static Date startDate; private static Date endDate; @BeforeClass public static void setUpValidatorAndDates() { ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); validator = validatorFactory.getValidator(); startDate = new GregorianCalendar(2009, 8, 20).getTime(); endDate = new GregorianCalendar(2009, 8, 21).getTime(); } @Test public void calendarEventIsValidAsEndDateIsAfterStartDate() { CalendarEvent testEvent = new CalendarEvent("Team meeting", startDate, endDate); assertTrue(validator.validate(testEvent).isEmpty()); } @Test public void calendarEventIsInvalidAsEndDateIsBeforeStartDate() { CalendarEvent testEvent = new CalendarEvent("Team meeting", endDate, startDate); Set<ConstraintViolation<CalendarEvent>> constraintViolations = validator.validate(testEvent); assertEquals(1, constraintViolations.size()); assertEquals( "Object couldn't be validated successfully.", constraintViolations.iterator().next().getMessage()); } } |
This works like a charm, only the error message returned by the BV runtime is not yet very expressive. This can easily be solved, as the BV API allows to override error messages within constraint annotations:
1 2 3 4 |
@SelfValidating(message="{de.gmorling.moapa.self_validating.CalendarEvent.message}") public class CalendarEvent implements Validatable { ... } |
Again we need an entry in ValidationMessages.properties for the specified message key:
1 |
de.gmorling.moapa.self_validating.CalendarEvent.message=End date of event must be after start date. |
Conclusion
Providing dedicated class-level constraints for all business objects that require some sort of cross-field validation logic can be quite a tedious task as you need to create an annotation type, a validator implementation and an error message text.
The @SelfValidating constraint reduces the required work by letting business objects validate themselves. That way all you have to do is implement the Validatable interface, annotate your class with @SelfValidating and optionally provide a customized error message. Furthermore business objects are not required to expose their internal state to make it accessible for an external validator implementation.
The complete source code can be found in my GitHub repository. As always I'd be happy about any feedback.