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;
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;
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;
}
}
Annotation @MappingTable
The enum type Maps
is used in the annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MappingTable {
Maps map();
}
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;
// ...
}
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();
}
}
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();
}
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;
}
}
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;
}
}
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();
}
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;
// ...
}
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());
}
}
}
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();
}
Annotation @MappingTable
The annotation @MappingTable
defines a list of MappingItem.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MappingTable {
MappingItem[] items();
}
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;
// ...
}
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
)
);
}
}
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)