Friday, October 21, 2022

Java enum generic serializer and deserializer

Java enum generic Serializer and Deserializer

 Introduction

Serializing a Java enum is simple using Jackson. It is easy and well known. 
 
If the enum has a single value then Jackson provides @JasonValue which serializes the enum as the value. However de-serializing becomes an issue. This leads you to start to use @JsonSerialize and @JsonDeserialize to control serialization. Soon you run into questions on how to get the Class of your enum passed into the deserializer and how to call the default deserializer hoping not to have to re-invent the wheel.

After using @JsonCreator, and trying to get the default serializer out of the Bean, I decided must be a way to do this and the way shouldn't clutter the enum with too many tags, deserializer subclasses for every enum type, etc.

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;
}