DEV Community

André Laugks
André Laugks

Posted on

Using a HashMap in a custom Annotation

Introduction

In my article "Create a custom Jackson JsonSerializer and JsonDeserializer for mapping values". I created the custom annotation @MappingTable to use key-value pairs for mapping. The key-value pairs are defined in a JSON and defined as a string in the @MappingTable annotation. In the MappingTableMapReader class, the JSON is converted into a HashMap and used in the JsonSerializer and JsonDeserializer.

@MappingTable(map = "{\"1\": \"MALE\", \"2\": \"FEMALE\", \"6\": \"DIVERS\"}")
private MappingValue<String> salutation;

@MappingTable(map = "{\"1\": true, \"2\": false}")
private MappingValue<Boolean> marketingInformation;
Enter fullscreen mode Exit fullscreen mode

One disadvantage of using JSON is that the key-value pairs cannot be reused. For example, if I want to use the key-value pairs in my application for validations, I would also have to define the key-value pairs as HashMap, which leads to duplication/redundancy. Therefore, using a HashMap is preferable.

Unsupported use of HashMap

The HashMap type is not directly supported by annotations. The following example will result in a compilation error.

public static Map<String, String> salutationMap = new HashMap<>() {{
    put("1", "MALE");
    put("2", "FEMALE");
    put("6", "DIVERS");
}};

@MappingTable(map = salutationMap)
private MappingValue<String> salutation;
Enter fullscreen mode Exit fullscreen mode

Supported types

Annotations only support the following types:

  • Primitive Typen (int, boolean, float etc.)
  • String
  • Enum
  • Class (Class<?>, Class<? extends/super T>)
  • Arrays der oben genannten Typen (int[], String[], Enum[] etc.)
  • Weitere Annotationen

Solutions

There are three possible options for using a HashMap in an annotation:


Example with Enum

Enum

The Maps enumeration type contains the constants SALUTATION and MARKETING_INFORMATION. The HashMaps are defined in these constants using Map.of(). Alternatively, new HashMap<>() can be used.

public enum Maps {

    SALUTATION(Map.of("1", "MALE", "2", "FEMALE", "6", "DIVERS")),

    MARKETING_INFORMATION(Map.of("1", true, "2", false));

    private final Map<String, Object> map;

    Maps(Map<String, Object> map) {
        this.map = map;
    }

    public Map<String, Object> getMap() {
        return this.map;
    }
}
Enter fullscreen mode Exit fullscreen mode

Annotation @MappingTable

The enum type Maps is used in the annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MappingTable {

    Maps map();
}
Enter fullscreen mode Exit fullscreen mode

ContactDto

The enum constants are referenced in the @MappingTable annotation.

public class ContactDto {

    // ...    

    @MappingTable(map = Maps.SALUTATION)
    private MappingValue<String> salutation;

    @MappingTable(map = Maps.MARKETING_INFORMATION)
    private MappingValue<Boolean> marketingInformation;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

MappingTableMapReader

In the class MappingTableMapReader, the HashMap is retrieved from the enum constant and can be used in the JsonSerializer and JsonDeserializer.

public class MappingTableMapReader {

    private final BeanProperty property;

    public MappingTableMapReader(BeanProperty property) {
        this.property = property;
    }

    public Map<String, Object> getMap() {

        MappingTable annotation = property.getAnnotation(MappingTable.class);

        if (annotation == null) {
            throw new MappingTableRuntimeException("Annotation @MappingTable not set at property");
        }

        return annotation.map().getMap();
    }
}
Enter fullscreen mode Exit fullscreen mode

Full example with Enum

https://github.com/alaugks/article-jackson-serializer/tree/mapping-table/enum.


Example with Classes

Classes

The SalutationMap and MarketingInformationMap implement the MapInterface interface.

MapInterface

public interface MapInterface {

    Map<String, Object> getMap();
}
Enter fullscreen mode Exit fullscreen mode

SalutationMap

public class SalutationMap implements MapInterface {

    @Override
    public Map<String, Object> getMap() {

        HashMap<String, Object> map = new HashMap<>();
        map.put("1", "MALE");
        map.put("2", "FEMALE");
        map.put("6", "DIVERS");
        return map;
    }
}
Enter fullscreen mode Exit fullscreen mode

MarketingInformationMap

public class MarketingInformationMap implements MapInterface {

    @Override
    public Map<String, Object> getMap() {

        HashMap<String, Object> map = new HashMap<>();
        map.put("1", true);
        map.put("2", false);
        return map;
    }
}
Enter fullscreen mode Exit fullscreen mode

Annotation @MappingTable

The annotation uses a restricted generic wildcard. Only classes with the MapInterface interface can be defined at the annotation.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MappingTable {

    Class<? extends MapInterface> mapClass();
}
Enter fullscreen mode Exit fullscreen mode

ContactDto

The classes are referenced in the @MappingTable annotation.

public class ContactDto {

    // ...    

    @MappingTable(mapClass = SalutationMap.class)
    private MappingValue<String> salutation;

    @MappingTable(mapClass = MarketingInformationMap.class)
    private MappingValue<Boolean> marketingInformation;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

MappingTableMapReader

In the MappingTableMapReader class, an instance of the class is created and the HashMap is retrieved with getMap() for use in the JsonSerialiser and JsonDeserialiser.

⚠ Instantiating classes with reflection (getDeclaredConstructor().newInstance()) can have a negative impact on performance.

public class MappingTableMapReader {

    private final BeanProperty property;

    public MappingTableMapReader(BeanProperty property) {
        this.property = property;
    }

    public Map<String, Object> getMap() {

        MappingTable annotation = property.getAnnotation(MappingTable.class);

        if (annotation == null) {
            throw new MappingTableRuntimeException("Annotation @MappingTable not set at property");
        }

        try {
            return annotation.mapClass()
                .getDeclaredConstructor()
                .newInstance()
                .getMap();

        } catch (ReflectiveOperationException e) {
            throw new MappingTableRuntimeException("Error instantiating map class: " + e.getMessage());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Full example with Class

https://github.com/alaugks/article-jackson-serializer/tree/mapping-table/class.


Example with nested Annotation

Annotationen

Nested annotations are only partially suitable because annotations do not support generic types, which limits the types of values.

Annotation @MappingItem

The annotation @MappingItem represents a single key-value pair.

@Target(ElementType.FIELD)
public @interface MappingItem {

    String fieldValueId();

    String value();
}
Enter fullscreen mode Exit fullscreen mode

Annotation @MappingTable

The annotation @MappingTable defines a list of MappingItem.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MappingTable {

    MappingItem[] items();
}
Enter fullscreen mode Exit fullscreen mode

ContactDto

The list of @MappingItem annotations are set in the @MappingTable annotation.

public class ContactDto {

    // ...    

    @MappingTable(items = {
        @MappingItem(fieldValueId = "1", value = "MALE"),
        @MappingItem(fieldValueId = "2", value = "FEMALE"),
        @MappingItem(fieldValueId = "6", value = "DIVERS")
    })
    private MappingValue<String> salutation;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

To also map this for marketingInformation, I would have to create the annotations @MappingTableString for the property salutation instead of @MappingTable and @MappingItemString instead of @MappingItem, as well as the annotations @MappingItemString and @MappingItemBoolean. This is not practical for my requirements.

MappingTableMapReader

The list of MappingItem entries is processed in the MappingTableMapReader class to convert the key-value pairs into a HashMap.

public class MappingTableMapReader {

    private final BeanProperty property;

    public MappingTableMapReader(BeanProperty property) {

        this.property = property;
    }

    public Map<String, String> getMap() {

        MappingTable annotation = property.getAnnotation(MappingTable.class);

        if (annotation == null) {
            throw new MappingTableRuntimeException("Annotation @MappingTable not set at property");
        }

        return Arrays
            .stream(annotation.items())
            .collect(
                Collectors.toMap(
                    MappingItem::fieldValueId,
                    MappingItem::value
                )
            );
    }
}
Enter fullscreen mode Exit fullscreen mode

Full example with nested Annotation

https://github.com/alaugks/article-jackson-serializer/tree/mapping-table/annotation.


Conclusion

Using enums offers clear advantages over classes or nested annotations, as they enable simple, reusable and easy-to-maintain implementations.

In contrast, classes require the use of reflection, which can have a potentially negative impact on performance. Nested annotations are of limited use due to the lack of generic and restricted type support.

Top comments (0)