Java Bean Validation: a Classic Example of Separation of Mechanism and Policy
Abstract
We recently discovered that Java Bean Validation is a very powerful, extendable way to validate the Java POJO. We found that by using Bean Validation, we can achieve the separation of mechanism and policy, which leads to an implementation that requires less code and easier to understand.
The Problem
In one of our projects, we need to validate the JSON input provided by the UI front-end in the back-end Java code. We have two requirements:
- We want to validate the JSON format. If the UI front-end provides a malformed JSON string, we want to throw an exception.
- After converting JSON to POJO, we want to validate the POJO. More specifically, we want to make sure:
1. A string field is within a certain length.
2. A Collection (e.g. List) has a certain size.
3. A Collection only contains unique elements.
4. A Collection has to contain certain elements.
If any of these constraints a violated, we need to throw exceptions that could help users to understand the problem. When a user provides a well-formed JSON string, but its content violates our constraints, we want to inform her which constraints are violated and why.
For the first requirement (validate the JSON format), our team has been using Jackson extensively, so Jackson is our go-to solution. It’s also able to detect if a required property is missing. Bean Validation (discussed later) also supports this.
For the second requirement, our initial implementation was to write our own code to validate the POJO converted from JSON using Jackson. The code looks like this:
if (!(obj.getStringField().length > 10 || obj.getStringField().length < 20)) {
throw new ConstriantViolationException(lengthViolationMessage);
}
if (obj.getCollectionField().size < 5) {
throw new ConstriantViolationException(collectionSizeViolationMessage);
}
// Other validations
Of course, the actual code is more complicated than this as we also need to pass the value into the exception message. So we end up with writing lots of message templates.
We quickly sensed that, although this solution works, it mixes the mechanism and policy. Policy here means the constraints we impose on the POJO. For example, “the length of this string has to be between 10 and 20” is a policy. Mechanism here means using Java code to check the length and throw an exception. Mixing mechanism and policy together has two major issues:
- The solution does not generalize very well. If we need to validate another POJO with a different set of constraints, we need to write similar code again.
- The policy is not apparent. We need to “decode” the policy by reading the validation code. Ideally, we want to declare those constraints in the POJO class.
Java Bean Validation 2.0
If you ever find yourself trying to solve a very common problem, chances are other people already solved that problem for you. This also applied to this problem. We found that Bean Validation 2.0 provide almost everything we want and more.
Bean Validation 2.0 is the successor of Bean Validation 1.1. Compared to 1.1, 2.0 supports Java 8, meaning that it supports java.util.Optional
new Date/Time types, and some new built-in constraints.
Since Bean Validation is a specification,javax-validation
jar itself does not provide the concrete implementation. We use hiberate-validator
and javax.el
to provide the implementations (see pom.xml
here).
Bean Validation is attractive because it allows us to use Java annotations to define constraints in the POJO class. The actual validation can happen elsewhere. This is the beauty of separation of mechanism and policy:
- By defining the policy (validation constraints) in the POJO class, we essentially set up a contract between the object producer and the object consumer. The constraint definitions are succinct and apparent. A bonus feature of Bean Validation: when defining a constraint, Bean Validation allows us to define a message template, which could contain placeholders for us to replace it with the specific value during the validation.
- The mechanism is manifest in two places. 1) Here we use
hiberate-validator
to provide the validation mechanism, but it’s possible to use other libraries to perform the validation. 2) The actual validation can be performed elsewhere if it even occurs. For example, it can happen on the producer side, or the client side. If the client trusts the data provided by the producer, it can choose not to validate the POJO at all.
Go back to our requirements:
- For the constraint “a string field is within a certain length”, we can use
@Size(min=10, max=20)
annotation. - For the constraint “a Collection (e.g. List) has a certain size”, we can use
@Size(min=10, max=20)
annotation as well. - For the constraint “a Collection only contains unique elements”, we realize there is no such annotation in Bean Validation specification. But since we use
hibernate-validator
, we foundUniqueElements
annotation in this library. Turns out it also provides many other useful annotations such asURL
,ISBN
, etc. All additional constraints can be found here. - For the constraint “a Collection has to contain certain elements”, we didn’t find an existing annotation in either Bean Validation specification or
hibernate-validator
. I do see the challenge for Bean Validation provides a generic specification for this use case: Java annotation only supports certain return types as specified in the Java Language Specification. Since we only care about theString
andInteger
elements, we decide to extend Bean Validation by creating custom constraints.
Define Our Own Constraints
We define new constraints ContainsStrings
and ContainsInts
, and we want to use them like this:
public class Customer {
@ContainsInts(values = {1, 2, 3, 4}, message = "The list misses required elements: {missingValues}")
List<Integer> scores;// Getter and setter
}
We want the validation to fail when scores
does not contain value 1,2,3,4
. And we want to tell the user what are those missing values. Example being if scores
is [1,2]
, our message should be The list misses required elements: 3,4
.
Both implementations can be found in my GitHub repository. Let’s take a look at ContainsInts
annotation in this post. The official tutorial on creating custom constraints can be found here.
The
Implementation
We define the annotation like this:
@Constraint(validatedBy = {ContainsIntValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ContainsInts { String message() default "{org.jeremy.ContainsInts.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int[] values();
}
Note that we specify ContainsIntValidator.class
in validatedBy
parameter. This allows the ContainsInts
annotation to use ContainsIntValidator
class to validate the collection. We implement ContainsIntValidator
as:
public class ContainsIntValidator implements ConstraintValidator<ContainsInts, Collection<Integer>> { private Set<Integer> values; @Override
public void initialize(ContainsInts constraintAnnotation) {
values = Arrays.stream(constraintAnnotation.values()).boxed().collect(Collectors.toSet());
} @Override
public boolean isValid(Collection<Integer> collection, ConstraintValidatorContext constraintValidatorContext) {
Set<Integer> missingValue = values.stream().filter(v -> !collection.contains(v)).collect(Collectors.toSet()); if (missingValue.isEmpty()) {
return true;
} if (constraintValidatorContext instanceof HibernateConstraintValidatorContext) {
constraintValidatorContext.unwrap(HibernateConstraintValidatorContext.class)
.addMessageParameter("missingValues", missingValue.stream().map(String::valueOf).collect(Collectors.joining(",")))
.withDynamicPayload(ImmutableSet.copyOf(missingValue));
} return false;
}
}
Notice that this class implements ConstraintValidator<ContainsInts, Collection<integer>>
. This means that this validation works on any Collection<Integer>
, such as List<Integer>
, Set<Integer>
, ArrayList<Integer>
, etc.
To be able to display the missingValues
in the constraint violation message, we use addMessageParameter
method to add a key-value pair that maps the missingValues
placeholder to all the missing values the validator found.
To test our own constraint and its validator, we create a Customer
object and set scores
to [1,2]
:
public class App {
public static void main(String[] args) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator(); Customer customer = new Customer();
customer.setScores(Lists.newArrayList(1, 2)); Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
violations.forEach(violation -> System.out.println(violation.getMessage()));
}
}
The output:
The list misses required elements: 3,4
Takeaways
After replacing our own validator code with Bean Validation, we believe that our code is much simpler and easier to read. We are also able to pull our own constraint annotation into a separate library, which can be reused in many other packages.
I also observed that creating new Java annotations to separate mechanism and policy is an uncharted area for many teams. Usually, people think that creating custom Java annotations is a more advanced solution than writing Java code. Maybe it is true. But I also believe that creating and using custom Java annotations usually brings greater rewards.
When writing code, always keep “separation of mechanism and policy” in mind. More often than not, dividing an implementation into mechanism and policy helps me imagine the new use cases beyond the current use case at hand. This allows me to come up with solutions that are able to scale out from solving one problem to solving multiple similar problems.