Java enum generic Serializer and Deserializer
Introduction
https://github.com/sirgilligan/EnumerationSerialization/blob/main/README.md
Java implementation for a Generic enum Serializer and Deserializer
Just like the traditional approach the enum's deserializer can be specified as a subclass of a base deserializer.
@JsonSerialize(using = EnumerationSerializer.class)
@JsonDeserialize(using = RGB.Deserializer.class)
@EnumJson(serializeProjection = Projection.NAME)
enum RGB {
RED,
GREEN,
BLUE;
public static class Deserializer extends EnumerationDeserializer<RGB> {
private static final long serialVersionUID = 1L;
public Deserializer() {
super(RGB.class);
}
}
}
This approach allows you to pass the enum Class. That is not bad, but it is a bit cluttered.
Instead the class of the enum can be specified via annotation!
@JsonSerialize(using = EnumerationSerializer.class)
@JsonDeserialize(using = EnumerationDeserializer.class)
@EnumJson(serializeProjection = Projection.NAME, deserializationClass = RGB.class)
enum RGB {
RED,
GREEN,
BLUE
}
This new annotation specifies the Class is RGB.class. It also specifies that the default representation on serializtion should be enum.name(). The default representation could be NAME, ORDINAL, VALUE, or ALIAS.
There are two special annotations to identify a value and an alias.
@JsonSerialize(using = EnumerationSerializer.class)
@JsonDeserialize(using = SomeDays.Deserializer.class)
@EnumJson(serializeProjection = Projection.ORDINAL)
enum SomeDays {
MONDAY("Lunes", "Monday"),
TUESDAY("Martes", "Tuesday"),
WEDNESDAY("Miercoles", "Wednesday");
@EnumJson(serializeProjection = Projection.VALUE)
final String value;
@EnumJson(serializeProjection = Projection.ALIAS)
final String alias;
SomeDays(String v, String a) {
this.value = v;
this.alias = a;
}
public static class Deserializer extends EnumerationDeserializer<SomeDays> {
private static final long serialVersionUID = 1L;
public Deserializer() {
super(SomeDays.class);
}
}
}
However EnumerationSerializer and EnumerationDeserialize uses reflection to find any member variable named "value" or "alias". This means you don't even have to use the annotation for VALUE or ALIAS if the naming convention is used. Therefore the above example can be de-cluttered and now look like this:
@JsonSerialize(using = EnumerationSerializer.class) @JsonDeserialize(using = SomeDays.Deserializer.class) @EnumJson(serializeProjection = Projection.ORDINAL,
deserializationClass =
SomeDays
.class)
) enum SomeDays { MONDAY("Lunes", "Monday"), TUESDAY("Martes", "Tuesday"), WEDNESDAY("Miercoles", "Wednesday"); final String value; final String alias; SomeDays(String v, String a) { this.value = v; this.alias = a; } }
I really like that! I like that alot.
The annotations can be applied to a class that has members of your enum.
static class SomeStuff {
//Serialize as the Ordinal
@EnumJson(serializeProjection = Projection.ORDINAL)
public SomeDays someDay = SomeDays.MONDAY;
//Serialize as the Identifier
@EnumJson(serializeProjection = Projection.NAME)
public SomeDays nextDay = SomeDays.values()[someDay.ordinal() + 1];
//Serialize as the Value
@EnumJson(serializeProjection = Projection.VALUE)
public SomeDays middleDay = SomeDays.WEDNESDAY;
//Serialize as the Alias
@EnumJson(serializeProjection = Projection.ALIAS)
public SomeDays tuesday = SomeDays.TUESDAY;
//Serialize as the Ordinal
@EnumJson(serializeProjection = Projection.ORDINAL)
public SomeNums aNum = SomeNums.ONE;
//Serialize as the Identifier
@EnumJson(serializeProjection = Projection.NAME)
public SomeNums anotherNum = SomeNums.TWO;
}
The Serializer
Use the link to the souce code to see the current implementation.
The code is included here so that you can use it as a reference on how to acheive something using serialization, reflection, or annotations.
https://github.com/sirgilligan/EnumerationSerialization/blob/main/src/main/java/org/example/EnumerationSerializer.java
/* EnumerationSerializer serializes an Enum looking for EnumJson annotations. If no EnumJson annotation is found the enum is serialized by name. If the enum is a member variable of some class then the EnumJson annotation at the member variable level is used and takes priority over any Enum class annotation. If there are no member variable EnumJson annotation then if there is an Enum class annotation it will be used. */ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import java.io.IOException; import java.lang.reflect.Field; import org.example.EnumJson.Projection; public class EnumerationSerializer<T extends Enum<T>> extends StdSerializer<Enum<T>> { private static final long serialVersionUID = 1L; public EnumerationSerializer() { this(null); } protected EnumerationSerializer(Class<Enum<T>> t) { super(t); } @SuppressWarnings("unchecked") @Override public void serialize(Enum<T> value, JsonGenerator gen, SerializerProvider provider) throws IOException { //Looking for data to write as json String dataToWrite = null; //Looking for a specific annotation //Field annotation has priority over class annotation EnumJson enumJsonAnnotation = null; //If this value is a field/member variable contained by another object, get the field name. String fieldName = gen.getOutputContext().getCurrentName(); if (null != fieldName) { //Does the field have an annotation? try { enumJsonAnnotation = gen.getOutputContext().getCurrentValue().getClass().getField(fieldName).getAnnotation(EnumJson.class); } catch (NoSuchFieldException ignored) { //ignored } } if (null == enumJsonAnnotation) { //There wasn't a field level annotation //Is there an annotation on the enum class? try { enumJsonAnnotation = value.getClass().getAnnotation(EnumJson.class); } catch (Exception ignored) { //ignored } } ObjectMapper mapper = (ObjectMapper) gen.getCodec(); if (null != enumJsonAnnotation) { switch (enumJsonAnnotation.serializeProjection()) { case ORDINAL: { dataToWrite = mapper.writeValueAsString(value.ordinal()); } break; case NAME: { dataToWrite = mapper.writeValueAsString(value.name()); } break; case ALIAS: { Field[] enumFields = value.getClass().getDeclaredFields(); Field field = findAnnotatedField(enumFields, Projection.ALIAS); if (null == field) { field = findFieldByName(Projection.ALIAS.name().toLowerCase(), (Class<T>) value.getClass()); } dataToWrite = getData(field, value, mapper); } break; case VALUE: { Field[] enumFields = value.getClass().getDeclaredFields(); Field field = findAnnotatedField(enumFields, Projection.VALUE); if (null == field) { field = findFieldByName(Projection.VALUE.name().toLowerCase(), (Class<T>) value.getClass()); } dataToWrite = getData(field, value, mapper); } break; default: break; } } else { //There was not any EnumJson annotation or known field such as value or alias. // Write enum as name dataToWrite = mapper.writeValueAsString(value.name()); } if (null != dataToWrite) { gen.writeRawValue(dataToWrite); } } protected Field findAnnotatedField(Field[] enumFields, EnumJson.Projection projection) { Field result = null; for (Field f : enumFields) { EnumJson annie = f.getAnnotation(EnumJson.class); if ((null != annie) && (annie.serializeProjection() == projection)) { result = f; break; } } return result; } private Field findFieldByName(String fieldName, Class<T> enumClass) { Field result = null; try { result = enumClass.getDeclaredField(fieldName); } catch (NoSuchFieldException ignored) { //Ignored } return result; } @java.lang.SuppressWarnings("java:S3011") protected String getData(Field field, Enum<T> value, ObjectMapper mapper) { String dataToWrite = null; Object foundValue = null; if (null != field) { try { field.setAccessible(true); foundValue = field.get(value); } catch (IllegalAccessException ignored) { //ignored } } if (null != foundValue) { try { dataToWrite = mapper.writeValueAsString(foundValue); } catch (JsonProcessingException ignored) { //ignored } } return dataToWrite; } }
The Deserializer
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; import java.lang.reflect.Field; import org.example.EnumJson.Projection; public class EnumerationDeserializer<T extends Enum<T>> extends StdDeserializer<Enum<T>> implements ContextualDeserializer { private static final long serialVersionUID = 1L; private transient Class<T> enumClass; private transient EnumJson classAnnotation = null; private transient EnumJson fieldAnnotation = null; protected EnumerationDeserializer() { this(null); } protected EnumerationDeserializer(Class<T> vc) { super(vc); this.enumClass = vc; } @SuppressWarnings("unchecked") @Override public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { //enumClass is not null if the constructor that takes the class as a param //is used to create this object. if (null != enumClass) { classAnnotation = enumClass.getAnnotation(EnumJson.class); } if (null != property) { fieldAnnotation = property.getAnnotation(EnumJson.class); if ((null == classAnnotation) && (null == enumClass)) { classAnnotation = property.getType().getRawClass().getAnnotation(EnumJson.class); if ((null != classAnnotation) && (classAnnotation.deserializationClass().isEnum())) { this.enumClass = (Class<T>) classAnnotation.deserializationClass(); } } } return this; } /*------------------------------------------------------------------------------------------- deserialize will check for four different matches on an enum. 1) If the json string matches the enum.name 2) If the enum nas an annotation for an EnumJson Projection = ALIAS 3) If the enum has an annotation for an EnumJson Projection = VALUE 4) If the json string matches the enum.ordinal -------------------------------------------------------------------------------------------*/ @Override public Enum<T> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { Enum<T> result = null; final String jsonValue = p.getText(); boolean caseInsensitive = false; if (fieldAnnotation != null) { caseInsensitive = fieldAnnotation.deserializeCaseInsensitive(); } else if (classAnnotation != null) { caseInsensitive = classAnnotation.deserializeCaseInsensitive(); } //------------------------------------------------------------------------------------------- //Check if json matches the Name for (final T enumValue : enumClass.getEnumConstants()) { if (enumValue.name().equals(jsonValue) || ((caseInsensitive) && enumValue.name().equalsIgnoreCase(jsonValue))) { result = enumValue; } } //------------------------------------------------------------------------------------------- //Check if the enum has an EnumJson Projection Annotation of ALIAS if (null == result) { Field[] enumFields = enumClass.getDeclaredFields(); result = enumByAnnotatedField(enumFields, Projection.ALIAS, jsonValue, caseInsensitive); } //------------------------------------------------------------------------------------------- //Check if the enum has an EnumJson Projection Annotation of VALUE if (null == result) { Field[] enumFields = enumClass.getDeclaredFields(); result = enumByAnnotatedField(enumFields, Projection.VALUE, jsonValue, caseInsensitive); } //------------------------------------------------------------------------------------------- //Check if json matches the Ordinal if (null == result) { for (final T enumValue : enumClass.getEnumConstants()) { if (Integer.toString(enumValue.ordinal()).equals(jsonValue)) { result = enumValue; } } } return result; } @java.lang.SuppressWarnings("java:S3011") protected Enum<T> enumByAnnotatedField(Field[] enumFields, EnumJson.Projection projection, String jsonValue, boolean caseInsensitive) { Enum<T> result = null; Field valueField = null; for (Field f : enumFields) { EnumJson annie = f.getAnnotation(EnumJson.class); if ((null != annie) && (annie.serializeProjection() == projection)) { valueField = f; break; } } if (null != valueField) { //The enum has a EnumJson Projection that matches valueField.setAccessible(true); try { for (final T enumValue : enumClass.getEnumConstants()) { //Get the projected value from the enum. Object projectedValue = valueField.get(enumValue); if ((null != projectedValue) && ((projectedValue.toString().equals(jsonValue)) || ((caseInsensitive) && projectedValue.toString().equalsIgnoreCase(jsonValue)))) { result = enumValue; } } } catch (IllegalAccessException ignored) { //ignored } } else { //Look for a field by named value or alias. if (projection == Projection.VALUE) { result = getFromKnownField(Projection.VALUE.name().toLowerCase(), jsonValue, caseInsensitive); } else if (projection == Projection.ALIAS) { result = getFromKnownField(Projection.ALIAS.name().toLowerCase(), jsonValue, caseInsensitive); } } return result; } protected Enum<T> getFromKnownField(String fieldName, String jsonValue, boolean caseInsensitive) { Enum<T> result = null; try { Field knownField = enumClass.getDeclaredField(fieldName); for (final T enumValue : enumClass.getEnumConstants()) { Object value = knownField.get(enumValue); if ((null != value) && ((value.toString().equals(jsonValue)) || ((caseInsensitive) && value.toString().equalsIgnoreCase(jsonValue)))) { result = enumValue; break; } } } catch (NoSuchFieldException | IllegalAccessException ignored) { //ignored } return result; } }
The Annotations
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @EnumAnnotation public @interface EnumJson { enum Projection { ALIAS, //Enum has a property for an alias or alternate name. E.g. Monday NAME, //Enum.name. E.g. MONDAY ORDINAL, //Enum.ordinal. E.g. 0, 1, 3, 4, etc. VALUE //Enum has property for some type of value. E.g. LUNES } Projection serializeProjection() default Projection.VALUE; boolean deserializeCaseInsensitive() default false; Class<?> deserializationClass() default Void.class; }