How to create custom bean validator with annotations

Hello, everybody! In this short article, I want to describe how you can create a simple bean validator with the annotation processor.

Let’s start. In each application we have beans. And in most cases those beans have restrictions on fields. And we want to have a simple instrument to work with them. We have several solutions: for example, we can validate fields inside our bean — in a constructor or setters. Or we can create a builder for this class and perform all the checks inside it. But if you have a simple check, for example, to check for null values in each field, you will need to write a lot of boilerplate code for the cases mentioned above. Let’s write our annotation validator.

First of all, we need to create an annotation. Let’s assume we have two checks in our custom validator: NotEmpty and Length. First, we will check that the field is not equal to null, or if it is a string, that the string is not empty. The second will check string length.

The code for these annotations will look like so:

@Retention(RUNTIME)
@Target(FIELD)
public @interface NotEmpty {}

Annotation is empty, we don’t need any extra parameters for it. And:

@Retention(RUNTIME)
@Target(FIELD)
public @interface Length {
int min() default 0;
int max() default Integer.MAX_VALUE;
}

This annotation checks string field length, so we need to know the string borders: max and min length.

Now we can add our annotation to test classes:

class NotNullEntity {
@NotEmpty
private String firstString;
}

And for another one:

class StringLengthEntity {
@Length(min = 5, max = 10)
private String firstString;
}

Those classes will help us test our validator.

Our validator should have one method that accepts one parameter. This parameter will be a NotNullEntity or StringLengthEntity instance. We don’t want to override methods for each class, so we need to accept the object:

public interface EntityValidator {
void validate(Object entity);
}

If our object is not valid, the validator will throw a RuntimeException:

public class AnnotationValidator implements EntityValidator {
@Override
public void validate(Object entity) {
… some logic… if(…) throw new RuntimeException(“….”);
}}

Now we should use reflection to inspect our object and find all our annotations. Function getAllFieldList() from apache commons lang library will help us. This function returns a list of java.lang.reflect.Field. We need to make all fields accessible to manipulate with them.

getAllFieldsList(entity.getClass())
.stream()
.peek(f -> f.setAccessible(true))
.forEach(field -> {
Annotation[] annotations = field.getAnnotations();
if (annotations.length == 0) return;
});

Using the function getAnnotation() we receive an array of all annotations on this field. If it is not empty we need to handle it.

First, let’s calculate field value:

Object fieldValue = getValue(field, entity);

private Object getValue(Field field, Object entity) {
try {
return field.get(entity);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}}

Then we need to iterate over all annotations:

for (Annotation annotation : annotations) {
doCheck(annotation, field.getName(), fieldValue);
}

First, the doCheck() function needs to verify our annotation. A field can have annotations from another framework. We need to separate it from ours.

I think the best idea is to store all our annotations in a special map inside AnnotationValidator. The key for this map will be our custom annotation. And value-object with Rule interface:

public interface Rule<T extends Annotation> {
void check(T annotation, String fieldName, Object target);
Class<T> getAnnotationClass();
}

This interface has two function declarations. The first function accepts annotation, field name, field value and performs validation, the second function returns the currently supported annotation. We will use it to get the key for the map.

We have an interface, let’s write implementations:

And for Lenght annotation:

Each rule can throw a different runtime exception, so we don’t need to add this logic to the annotation validator, as assumed above.

Our map will look now that:

private final Map<Class<? extends Annotation>, Rule<?>> rules;

And we can initialize it in AnnotationValidator constructor:

public AnnotationValidator(List<Rule<?>> rules) {
this.rules = rules.stream()
.collect(toMap(Rule::getAnnotationClass, identity()));}

Let’s return to the doCheck() function. JVM has a little strange behavior — no annotations in runtime are original annotations. All of them are wrapped by JVM, so we can’t simply get a class name from the annotation parameter. It will always be sun.proxy class. But we can call the annotationType() function from it, which will return the genuine annotation:

private void doCheck(Annotation annotation, String field, Object fieldValue) {
Rule r = rules.get(annotation.annotationType());
if (r == null) return;
r.check(annotation, field, fieldValue);
}

So if we can’t find annotation in map we skip this check or call rule method check(). Our annotation validator is ready :)

That is all. Thank you for attention!

Full code with tests you can find in my GitHub: https://github.com/r331/annotationvalidator.git

Passionate about Java and quantum mechanics

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store