Showing posts with label serialization. Show all posts
Showing posts with label serialization. Show all posts

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

 

 

Wednesday, January 05, 2022

Boxing an Array in Swift

Swift Arrays

If you are reading this you probably have the need to work with an array returned from a function or a copy of an array. I have been porting a complex data transfer object (DTO) from Java to Swift. The Java implementation of this DTO has many violations the Law of Demeter.

The Swift implementation needs to resemble the Java implementation so that changes to one code base will be easier to make in the other code base. This requirement lead me to creating a Boxed Array which I named "List".  Before critiquing the name of the class or the method names, this is to make the port from Java as simple as possible FOR ME. You can make List implement the methods of a Swift Array if you want, or make Protocols for your own "collection classes".

This example of Boxing / Wrapping a Swift Array is as simple as possible. This includes the ability to serialize the List class to be the same as an Array when serialized to JSON.



//Geoffrey Slinker - Just keep my name here please.
import Foundation


public class List<T : Equatable & Codable> : Sequence, Codable
{
    var container : [T] = []
    
    public init() {
        
    }
    
    //------------------------------------------------------------------------
    //JSON Serialization
    required public init(from decoder:Decoder) throws {
        try container = [T](from: decoder)
    }
    
    public func encode(to encoder: Encoder) throws {
        try container.encode(to: encoder)
    }
    //------------------------------------------------------------------------
    
    public func makeIterator() -> ListIterator<T> {
        return ListIterator(self)
    }
    
    public func append(_ newElement:T) {
        container.append(newElement)
    }
    
    public func get(_ at : Int) -> T? {
        return container[at]
    }
    
    public func remove(at : Int) {
        container.remove(at: at)
    }
    
    public func remove(_ element:T) {
        if let index = container.firstIndex(where: {$0 == element}) {
            container.remove(at: index)
        }
    }
    
    public func clear() {
        container.removeAll()
    }
    
    public var count : Int {
        get {
            return container.count
        }
    }
}

public struct ListIterator<T : Equatable & Codable> : IteratorProtocol
{
    let list : List<T>
    var currentPosition = 0
    
    init(_ list: List<T>) {
        self.list = list
    }
    
    public mutating func next() -> T? {
        if(currentPosition < list.count) {
            let result = list.get(currentPosition)
            currentPosition += 1
            return result
        }
        return nil
    }
    
}

Test Code

import XCTest

class ListTests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }


    func testJson() throws {
        let myArray = [0, 1, 2, 3]
        
        let arrayData = try! JSONSerialization.data(withJSONObject: myArray, options: [])
        let arrayJson = String(data: arrayData, encoding: String.Encoding.utf8)
        
        
        let myList : List<Int> = List<Int>()
        myList.append(0)
        myList.append(1)
        myList.append(2)
        myList.append(3)
        

        let listData = try! JSONEncoder().encode(myList)
        let listJson = String(data: listData, encoding: String.Encoding.utf8)
        
        XCTAssertEqual(arrayJson, listJson)
        

        let anotherList : List<Int>? = try JSONDecoder().decode(List<Int>.self, from: listData)
        XCTAssert(anotherList!.count == 4)
        XCTAssertEqual(anotherList?.get(0), 0)
        XCTAssertEqual(anotherList?.get(1), 1)
        XCTAssertEqual(anotherList?.get(2), 2)
        XCTAssertEqual(anotherList?.get(3), 3)
        
        let yetAnotherList : List<Int>? = try JSONDecoder().decode(List<Int>.self, from: arrayData)
        XCTAssert(yetAnotherList!.count == 4)
        XCTAssertEqual(yetAnotherList?.get(0), 0)
        XCTAssertEqual(yetAnotherList?.get(1), 1)
        XCTAssertEqual(yetAnotherList?.get(2), 2)
        XCTAssertEqual(yetAnotherList?.get(3), 3)
    }

}