Skip to main content

Java Annotations

Annotations are a form of metadata that provide data about a program but are not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

They are widely used by compiler tools (to detect errors or suppress warnings), build-time tools (to generate code or XML configuration files), and runtime frameworks (such as Spring, Hibernate, or Jackson) to dynamically configure and customize program behavior.


Meta-Annotations

Meta-annotations are annotations that are applied to other annotations. They define the behavior and scope of your custom annotations.

1. @Retention

Defines how long the annotation should be kept:

  • RetentionPolicy.SOURCE: Discarded by the compiler; not present in the compiled .class file (e.g. @Override).
  • RetentionPolicy.CLASS: Recorded in the .class file but discarded by the JVM at runtime (this is the default).
  • RetentionPolicy.RUNTIME: Retained in the .class file and loaded by the JVM at runtime. Must be set to RUNTIME if you want to read the annotation via reflection.

2. @Target

Defines where the annotation can be applied. Common ElementType targets include:

  • ElementType.TYPE: Class, interface, or enum
  • ElementType.FIELD: Fields or variables
  • ElementType.METHOD: Methods
  • ElementType.PARAMETER: Method parameters
  • ElementType.CONSTRUCTOR: Constructors

Creating a Custom Annotation

Custom annotations are defined using the @interface keyword. The methods in the interface define the properties of the annotation.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD) // Can only be used on fields
@Retention(RetentionPolicy.RUNTIME) // Can be inspected at runtime
public @interface JsonField {
String value() default ""; // An element with a default value
}

Practical Code Example: Custom JSON Serializer

Here is a complete example of creating a custom annotation @JsonField and writing a processing class that uses reflection to serialize any object to a JSON string using those annotations.

Step 1: The Annotation and Model

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// Define custom annotation
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface JsonField {
String key() default ""; // Maps the field to a custom JSON key
}

// Class using custom annotations
public class Book {
@JsonField(key = "book_title")
private String title;

@JsonField(key = "author_name")
private String author;

// This field has no annotation and will be ignored by our serializer
private double price;

public Book(String title, String author, double price) {
this.title = title;
this.author = author;
this.price = price;
}
}

Step 2: The Annotation Processor (Serializer)

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class JsonSerializer {
public static String serialize(Object obj) {
Class<?> clazz = obj.getClass();
Map<String, String> jsonMap = new HashMap<>();

// Loop through all fields of the class
for (Field field : clazz.getDeclaredFields()) {
// Check if our custom annotation is present
if (field.isAnnotationPresent(JsonField.class)) {
field.setAccessible(true); // Allow accessing private fields
try {
// Get the annotation instance
JsonField annotation = field.getAnnotation(JsonField.class);

// Determine the JSON key (use the custom key or fall back to field name)
String key = annotation.key().isEmpty() ? field.getName() : annotation.key();
Object value = field.get(obj);

jsonMap.put(key, value == null ? "null" : "\"" + value.toString() + "\"");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

// Format map as a JSON string: {"key1": "val1", "key2": "val2"}
String jsonString = jsonMap.entrySet().stream()
.map(entry -> entry.getKey() + ":" + entry.getValue())
.collect(Collectors.joining(","));

return "{" + jsonString + "}";
}

public static void main(String[] args) {
Book book = new Book("Effective Java", "Joshua Bloch", 45.00);

// Serialize the Book object to JSON
String json = JsonSerializer.serialize(book);
System.out.println("Serialized JSON: " + json);
// Output: {book_title:"Effective Java",author_name:"Joshua Bloch"}
}
}

[!TIP] Notice how the price field was omitted from the JSON output because it didn't have the @JsonField annotation. This is exactly how frameworks like Jackson, Gson, or Spring serialize and map resources under the hood!