Gson Serialize a List of Polymorphic Objects

Gson serialize a list of polymorphic objects

I think that a custom serializer/deserializer is the only way to proceed and I tried to propose you the most compact way to realize it I have found. I apologize for not using your classes, but the idea is the same (I just wanted at least 1 base class and 2 extended classes).

BaseClass.java

public class BaseClass{

@Override
public String toString() {
return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]";
}

public ArrayList<BaseClass> list = new ArrayList<BaseClass>();

protected String isA="BaseClass";
public int x;

}

ExtendedClass1.java

public class ExtendedClass1 extends BaseClass{

@Override
public String toString() {
return "ExtendedClass1 [total=" + total + ", number=" + number
+ ", list=" + list + ", isA=" + isA + ", x=" + x + "]";
}

public ExtendedClass1(){
isA = "ExtendedClass1";
}

public Long total;
public Long number;

}

ExtendedClass2.java

public class ExtendedClass2 extends BaseClass{

@Override
public String toString() {
return "ExtendedClass2 [total=" + total + ", list=" + list + ", isA="
+ isA + ", x=" + x + "]";
}

public ExtendedClass2(){
isA = "ExtendedClass2";
}

public Long total;

}

CustomDeserializer.java

public class CustomDeserializer implements JsonDeserializer<List<BaseClass>> {

private static Map<String, Class> map = new TreeMap<String, Class>();

static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}

public List<BaseClass> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {

List list = new ArrayList<BaseClass>();
JsonArray ja = json.getAsJsonArray();

for (JsonElement je : ja) {

String type = je.getAsJsonObject().get("isA").getAsString();
Class c = map.get(type);
if (c == null)
throw new RuntimeException("Unknow class: " + type);
list.add(context.deserialize(je, c));
}

return list;

}

}

CustomSerializer.java

public class CustomSerializer implements JsonSerializer<ArrayList<BaseClass>> {

private static Map<String, Class> map = new TreeMap<String, Class>();

static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}

@Override
public JsonElement serialize(ArrayList<BaseClass> src, Type typeOfSrc,
JsonSerializationContext context) {
if (src == null)
return null;
else {
JsonArray ja = new JsonArray();
for (BaseClass bc : src) {
Class c = map.get(bc.isA);
if (c == null)
throw new RuntimeException("Unknow class: " + bc.isA);
ja.add(context.serialize(bc, c));

}
return ja;
}
}
}

and now this is the code I executed to test the whole thing:

public static void main(String[] args) {

BaseClass c1 = new BaseClass();
ExtendedClass1 e1 = new ExtendedClass1();
e1.total = 100L;
e1.number = 5L;
ExtendedClass2 e2 = new ExtendedClass2();
e2.total = 200L;
e2.x = 5;
BaseClass c2 = new BaseClass();

c1.list.add(e1);
c1.list.add(e2);
c1.list.add(c2);

List<BaseClass> al = new ArrayList<BaseClass>();

// this is the instance of BaseClass before serialization
System.out.println(c1);

GsonBuilder gb = new GsonBuilder();

gb.registerTypeAdapter(al.getClass(), new CustomDeserializer());
gb.registerTypeAdapter(al.getClass(), new CustomSerializer());
Gson gson = gb.create();

String json = gson.toJson(c1);
// this is the corresponding json
System.out.println(json);

BaseClass newC1 = gson.fromJson(json, BaseClass.class);

System.out.println(newC1);

}

This is my execution:

BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
{"list":[{"total":100,"number":5,"list":[],"isA":"ExtendedClass1","x":0},{"total":200,"list":[],"isA":"ExtendedClass2","x":5},{"list":[],"isA":"BaseClass","x":0}],"isA":"BaseClass","x":0}
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]

Some explanations: the trick is done by another Gson inside the serializer/deserializer. I use just isA field to spot the right class. To go faster, I use a map to associate the isA string to the corresponding class. Then, I do the proper serialization/deserialization using the second Gson object. I declared it as static so you won't slow serialization/deserialization with multiple allocation of Gson.

Pro
You actually do not write more code than this, you let Gson do all the work. You have just to remember to put a new subclass into the maps (the exception reminds you of that).

Cons
You have two maps. I think that my implementation can refined a bit to avoid map duplications, but I left them to you (or to future editor, if any).

Maybe you want to unify serialization and deserialization into a unique object, you should be check the TypeAdapter class or experiment with an object that implements both interfaces.

GSON Serialize Polymorphic Object with Type Stored in a Different Object

I realize that asking for a solution that doesn't involve a custom Serializer/Deserializer is a bit ridiculous, as this is exactly the type of scenario they'd be used in (I was thinking I could get away with a custom TypeAdapterFactory, but using a Serializer/Deserializer is easier).

Anyway, for my scenario, a combination of a custom Serializer/Deserializer for the Message class seems to work fine. Since I already use an enum to track different message purposes and their string names, I decided to simply add an additional field to that enum to store the corresponding body class.

MessagePurpose Enum:

public enum MessagePurpose {
EVENT("event", EventBody.class);

public final String purposeName;
public final Class bodyClass;

MessagePurpose(String purposeName, Class classi) {
this.purposeName = purposeName;
bodyClass = classi;
}
}

MessageSerializer:

public class MessageSerializer implements JsonSerializer<Message> {
@Override
public JsonElement serialize(Message message, Type type, JsonSerializationContext jsc) {
if(message == null) {
return null;
}

JsonObject messageObj = new JsonObject();

// Get the class representing the body object from the purpose enum
Class bodyClassType = message.getPurpose().bodyClass;
messageObj.add("body", jsc.serialize(message.getBody(), bodyClassType));

messageObj.add("header", jsc.serialize(message.getHeader(), Header.class));

return messageObj;
}
}

MessageDeserializer:

public class MessageDeserializer implements JsonDeserializer<Message> {
@Override
public Message deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
Header header = jdc.deserialize(je.getAsJsonObject().get("header"), Header.class);

// Get the class representing the body object from the purpose enum
Class bodyClassType = header.getPurpose().bodyClass;
Body body = jdc.deserialize(je.getAsJsonObject().get("body"), bodyClassType);

return new Message(body, header);
}
}

Main function to test with:

public static void main(String[] args) {
GsonBuilder gb = new GsonBuilder();

// Register the Message class since I need to access info in the header
gb.registerTypeAdapter(Message.class, new MessageDeserializer());
gb.registerTypeAdapter(Message.class, new MessageSerializer());

Gson gson = gb.setPrettyPrinting().create();

EventBody event = new EventBody(EventType.SOME_EVENT_NAME);

String eventJson = gson.toJson(event.getAsMessage());
System.out.println(eventJson);

Message newEvent = gson.fromJson(eventJson);
System.out.println("\nEvent type: " + ((EventBody) newEvent.getBody()).getEventName());
}

The above test class prints:

{
"body": {
"eventType": "someEventName"
},
"header": {
"purpose": "event"
}
}

Event Type: someEventName

This output matches the JSON of the Messages I'm parsing, and it seems to deserialize different types of messages just fine.

Polymorphic deserialization gson

So I wrote a custom deserializer for Response where I first invoke the default deserializer, get the vehicle type and do Vehicle deserialization accordingly.

public Response deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
Response response = new Gson().fromJson(jsonElement, Response.class);
Type t = null;
if (response.getVehicleType().equals(Bus.vehicle_type)) {
t = Bus.class;
} else if (response.getVehicleType().equals(Car.vehicle_type)) {
t = Car.class;
}
JsonObject object = jsonElement.getAsJsonObject();
if (object.has("vehicle")) {
JsonElement vehicleElement = object.get("vehicle");
Vehicle vehicle = jsonDeserializationContext.deserialize(vehicleElement, t);
response.setVehicle(vehicle);
}
return response;
}

Looking forward to better solutions. :)

Deserialize List of polymorphic objects into Object field

I express sincere gratitude to all who participated. I decided to go with a little bit introspective approach and wrote custom deserializer that looks inside first object in list to figure out if it needs to deserialize this as List.

public class AnimalsDeserializer extends JsonDeserializer<Object> {

private Set<String> subtypes = Set.of("dog", "cat");

@Override
public Object deserialize(JsonParser parser, DeserializationContext ctx)
throws IOException, JsonProcessingException {
if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
TreeNode node = parser.getCodec().readTree(parser);
TreeNode objectNode = node.get(0);
if (objectNode == null) {
return List.of();
}
TreeNode type = objectNode.get("name");
if (type != null
&& type.isValueNode()
&& type instanceof TextNode
&& subtypes.contains(((TextNode) type).asText())) {
return parser.getCodec().treeAsTokens(node).readValueAs(new TypeReference<ArrayList<Animal>>() {
});
}
}
return parser.readValueAs(Object.class);
}
}

Maybe some checks are redundant. It works and it's all that matters :).
Award goes to StephanSchlecht as author of most inspirational answer. Thanks again!

Consuming polymorphic json data: { put_anything_here } with Gson & Retrofit

                val frameTextReceived: String = frame.readText()
val jsonObject = JsonParser.parseString(frameTextReceived).asJsonObject
val type = when (jsonObject.get("type").asString) {
TYPE_JOIN_ROOM -> JoinRoom::class.java
TYPE_GAME_MOVE -> GameMove::class.java
TYPE_DISCONNECT_REQUEST -> DisconnectRequest::class.java
else -> BaseModel::class.java
}
val payload = gson.fromJson(frameTextReceived, type)

This is my solution, here I have type parameter by which I can know in which class I have to deserialize the object but in your case you have screen parameter, you can use this.



Related Topics



Leave a reply



Submit