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;
}
No comments:
Post a Comment