How to Create Custom Field Validator Annotation In Java

  • Post last modified:September 27, 2023
  • Reading time:6 mins read

Creating custom annotation in Java for field validation

1. Introduction

  • Java annotation are a form of metadata that can be added to the code which provides information to the compiler or runtime environment.
  • This information can be used for various use cases such as providing hint to the compiler, adding code generation or providing information to frameworks etc.
  • In this article learn about annotation and will create annotation for field validation in class.

2. Annotation Explained!

  • Java Annotations are kind of markers that can be added to classes, methods , fields etc.
  • By adding annotation to the Java classes, methods or fields , it provides metadata information to the compiler or runtime environment.
  • Annotation start with “@” symbol and can be built in or custom.
  • For example, `@Component`, `@Repository` are example of built in annotation in Spring framework.

Annotation Interface

  • First we need to define our annotation `CustomAnnotation` as interface.
  • We need to mark this annotation with @Retention and @Target .
  • @Retention (RetentionPolicy.RUNTIME) specifies that annotation will be retained at runtine.
  • @Target ({ ElementType.TYPE, ElementType.METHOD }) restricts annotation to be applicable on `Class` and `Method`.
@Retention(RetentionPolicy.RUNTIME) // when annotation will be available
@Target({ ElementType.TYPE, ElementType.METHOD }) // where annotation can be applied
public @interface CustomAnnotation {
    String value() default "";
    int count() default 0;
}

Marking Annotation

  • Now lets mark target class and method with the annotation. We need to pass the metadata info which value and count that we defined in annotation.
@CustomAnnotation(value = "a_class", count = 2)
public class MyClass {

    @CustomAnnotation(value = "a_method", count = 1)
    public static void method(){

    }
}

Reading metadata

  • Now that we have annotation defined and used, lets read them at runtime.
  • We can get class level metadata using `Class.getAnnotation()` and method level metadata with `Method.getAnnotation()`.
  • Once we have annotation instance we can access its property value by simple method call.
  • In order to access value of class , we can just call `annotation.value()`.
private static void example1() throws NoSuchMethodException {
     Class<MyClass> myClass = MyClass.class;
     CustomAnnotation annotation = myClass.getAnnotation(CustomAnnotation.class);
        
     String value = annotation.value();
     int count = annotation.count();
     System.out.println("value: "+value+", count: "+count);

     Method method = myClass.getMethod("method");
     CustomAnnotation annotation1 = method.getAnnotation(CustomAnnotation.class);
        
     System.out.println("value: "+annotation1.value());
     System.out.println("count: "+annotation1.count());
}

Output

  • As we can see below we got class & method metadata value .
value: a_class, count: 2
value: a_method
count: 1

Now lets create our own annotation to validate fields in Java class.

3. Creating Custom Field Validator Annotation

  • Now lets extend our basic example and create something useful.
  • Lets create two annotation that will help us validating field in java class.
  • We will create @NotNull and @Length annotation.
  • @NotNull: it will check if the annotated field is non null . if its null then throws exception
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotNull {
      String message() default "Field cannot be null";
}
  • @Length: it will check if the annoted field respects min or max length passed to this annotation.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Length {
    int max() default Integer.MAX_VALUE;
    int min() default Integer.MIN_VALUE;
}
  • Lets create `Customer` POJO / record and annotate filed with appropriate annotation.
  • For password we are making sure its not null and min size 28 and max 100.
  • For username and emailId we are checking if its not null.
public record Customer(
    @NotNull(message = "username must not be null")
    String username,
    @NotNull(message = "password must not be null") 
    @Length(min = 28, max = 100)
    String password,
    @NotNull(message = "emailId must not be null")
    String emailId
) { }
  • Now lets write validation logic that will read above annotation and metadata at runtime, then perform validation and throw exception with the provided message if required.
  • At first lets get all the fields from customer record. 
  • Now lets iterate over each field and get all the annotation for that field.
  • For each annotation we get value and handle it based on annotation type.
public static void validate(Object object) throws IllegalAccessException {
        List<Class<? extends Annotation>> annotations = List.of(NotNull.class, Length.class);

        Class<?> clazz = object.getClass();
        Field[] declaredFields = clazz.getDeclaredFields();
        System.out.println(Arrays.toString(declaredFields));
        for(Field field: declaredFields) {
            List<? extends Class<? extends Annotation>> targetAnnotations = getTargetAnnotations(annotations, field);
            targetAnnotations.forEach(
                a -> {
                    field.setAccessible(true);
                    String typeName = a.getSimpleName();
                    Object fieldVal = getObject(object, field);
                    handle(field, typeName, fieldVal);
                }
            );
        }
    }
  • Here is the logic to get object value.
    private static Object getObject(Object object, Field field) {
        Object fieldVal;
        try {
            fieldVal = field.get(object);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        return fieldVal;
    }
  • Here is the logic to get all the annoation type for the field.
private static List<? extends Class<? extends Annotation>> getTargetAnnotations(List<Class<? extends Annotation>> annotations, Field field) {
        return Arrays.stream(field.getAnnotations())
            .map(Annotation::annotationType)
            .filter(annotations::contains)
            .toList();
    }
  • Handle logic takes the typeName of the annotation and operate on it.
  • For NotNull we check if the fieldVal null , if it is then we extract the message and throw `IllegalArgumentException`.
  • For Length, we are extracting min and max length , then we are checking if the fieldVal length lies in that range or not.
  • If not then we throw `IllegalArgumentException` with custom error message.
 private static void handle(Field field, String typeName, Object fieldVal) {
        switch (typeName) {
            case "NotNull" -> {
                if (fieldVal != null)
                    return;
                String message = field.getAnnotation(NotNull.class)
                    .message();
                throw new IllegalArgumentException(message);
            }
            case "Length" -> {
                Length lengthAnnotation = field.getAnnotation(Length.class);
                int max = lengthAnnotation.max();
                int min = lengthAnnotation.min();

                if (fieldVal.toString().length() >= min && fieldVal.toString().length() <= max)
                    return;

                String message = "length should be in the range of min %d and max %d".formatted(min, max);
                throw new IllegalArgumentException(message);
            }
        }
    }

Output

  • it throws password length not matching error.
Exception in thread "main" java.lang.IllegalArgumentException: length should be in the range of min 28 and max 100
 at org.example.Annotation.FieldValidator.handle(FieldValidator.java:83)
 at org.example.Annotation.FieldValidator.lambda$validate$0(FieldValidator.java:42)
 at java.base/java.lang.Iterable.forEach(Iterable.java:75)
 at org.example.Annotation.FieldValidator.validate(FieldValidator.java:37)
 at org.example.Annotation.Main.example3(Main.java:16)
 at org.example.Annotation.Main.main(Main.java:10)

4. Conclusion

  • Annotation are used extensively in Java developement and certainly its all over used inside Framework like Spring.
  • In this article we learnt about Annotation in Java. We also build our own field validator annotation.

Before You Leave

Leave a Reply