Polymorphism with Gson

How to handle deserializing with polymorphism?

In the Gson project code base is the RuntimeTypeAdapter, which reportedly works well for polymorphic serialization and deserialization. I don't think I've yet tried to use it. See http://code.google.com/p/google-gson/issues/detail?id=231 for more info. Note, it hasn't yet been included in any Gson releases.

If use of it doesn't fit your needs, then custom deserialization processing is necessary. Following is one such approach, assuming you want to use the JSON structure demonstrated. (I'd take a somewhat different approach, if the JSON structure could be different.)

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;

public class App
{
public static void main(String[] args)
{
Barn[] barns = {new Barn(), new Barn()};
barns[0].type = "horse";
barns[0].animal = new Horse();
barns[1].type = "cow";
barns[1].animal = new Cow();

String json = new Gson().toJson(barns);
// [{"type":"horse","animal":{}},{"type":"cow","animal":{}}]

BarnDeserializer deserializer = new BarnDeserializer("type");
deserializer.registerBarnType("horse", Horse.class);
deserializer.registerBarnType("cow", Cow.class);
Gson gson = new GsonBuilder().registerTypeAdapter(Barn.class, deserializer).create();

List<Barn> barns2= gson.fromJson(json, new TypeToken<List<Barn>>(){}.getType());
for (Barn barn : barns2)
{
System.out.println(barn.animal.getClass());
}
}
}

class BarnDeserializer implements JsonDeserializer<Barn>
{
String barnTypeElementName;
Gson gson;
Map<String, Class<? extends Animal>> barnTypeRegistry;

BarnDeserializer(String barnTypeElementName)
{
this.barnTypeElementName = barnTypeElementName;
gson = new Gson();
barnTypeRegistry = new HashMap<>(); // Java 7 required for this syntax.
}

void registerBarnType(String barnTypeName, Class<? extends Animal> animalType)
{
barnTypeRegistry.put(barnTypeName, animalType);
}

@Override
public Barn deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException
{
JsonObject barnObject = json.getAsJsonObject();
JsonElement animalTypeElement = barnObject.get(barnTypeElementName);
Barn barn = new Barn();
barn.type = animalTypeElement.getAsString();
Class<? extends Animal> animalType = barnTypeRegistry.get(barn.type);
barn.animal = gson.fromJson(barnObject.get("animal"), animalType);
return barn;
}
}

class Barn {String type; Animal animal;}
class Animal {}
class Horse extends Animal {}
class Cow extends Animal {}

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.

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. :)

Deserializing polymorphic JSON with transient variables using Gson-extras

Everything is fine with type adapters and you can use them both at the same time. In your case, problem is in order in which code is executed.

  1. GsonUtils.getGson() - creates Gson object with adapters.
  2. gson.toJson(new DerivedClass(6), BaseClass.class) - you create DerivedClass instance which executes super constructor from BaseClass which register given class in adapter (sic!).

Try this code:

DerivedClass src = new DerivedClass(6);
Gson gson1 = GsonUtils.getGson();
String serialized = gson1.toJson(src, BaseClass.class);
System.out.println(serialized);
DerivedClass dc = gson1.fromJson(serialized, DerivedClass.class);

It will work as expected. Creating DerivedClass has a side effect - this is a really good example why classes should be decoupled from each other. You should not have any Gson specific class in BaseClass.

In the best world BaseClass should contain only common properties and some base logic:

class BaseClass {
// getters, setters, business logic.
}

All Gson configuration should be placed in GsonUtils class:

class GsonUtils {

public static Gson getGson() {
return new GsonBuilder()
.registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(BaseClass.class)
.registerSubtype(DerivedClass.class))
.registerTypeAdapterFactory(new PostConstructAdapterFactory())
.create();
}
}

If you do not want to specify all classes manually, you need to scan environment in runtime. Take a look at: At runtime, find all classes in a Java application that extend a base class.

Edit after question was updated

It looks like TypeAdapterFactory chaining is not so straightforward as I would expect. It depends from given TypeAdapterFactory implementation whether it returns null or returns new object with some delegation to other factories.

I found a workaround but it could be not easy to use in real project:

  1. I created two Gson objects: one for serialisation and one for deserialisation. Only for deserialisation process we register PostConstructAdapterFactory.
  2. We add a method with @PostConstruct annotation to BaseClass.

Example:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.typeadapters.PostConstructAdapterFactory;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;

import javax.annotation.PostConstruct;

public class GsonApp {

public static void main(String[] args) {
RuntimeTypeAdapterFactory<BaseClass> typeFactory = RuntimeTypeAdapterFactory.of(BaseClass.class)
.registerSubtype(DerivedClass.class);

Gson serializer = new GsonBuilder()
.registerTypeAdapterFactory(typeFactory)
.create();

Gson deserializer = new GsonBuilder()
.registerTypeAdapterFactory(typeFactory)
.registerTypeAdapterFactory(new PostConstructAdapterFactory())
.create();

WrapperClass wrapper = new WrapperClass();
wrapper.setDc(new DerivedClass(8));

String json = serializer.toJson(wrapper);
System.out.println(json);
System.out.println(deserializer.fromJson(json, WrapperClass.class));
}

}

class WrapperClass {
protected BaseClass dc;

public BaseClass getDc() {
return dc;
}

public void setDc(BaseClass dc) {
this.dc = dc;
}

@Override
public String toString() {
return "WrapperClass{" +
"dc=" + dc +
'}';
}
}

class BaseClass {

@PostConstruct
protected void postConstruct() {
}
}

class DerivedClass extends BaseClass {
public DerivedClass(Integer number) {
super();
this.number = number;
Init();
}

protected Integer number;
protected transient Integer doubled;
protected transient Integer tripled;
protected transient Integer halved;
protected transient Integer squared;
protected transient Integer cubed;

public void Init() {
halved = number / 2;
doubled = number * 2;
tripled = number * 3;
squared = number * number;
cubed = number * number * number;
}

@PostConstruct
protected void postConstruct() {
Init();
}

@Override
public String toString() {
return "DerivedClass{" +
"number=" + number +
", doubled=" + doubled +
", tripled=" + tripled +
", halved=" + halved +
", squared=" + squared +
", cubed=" + cubed +
"} ";
}
}

Above code prints:

{"dc":{"type":"DerivedClass","number":8}}
WrapperClass{dc=DerivedClass{number=8, doubled=16, tripled=24, halved=4, squared=64, cubed=512} }

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.

Parsing Nested Polymorphic Objects with GSON and Retrofit

RuntimeTypeAdapterFactory requires the viewHolderType field to be put right into the body objects. In order to fix this, you have
either patch RuntimeTypeAdapterFactory (it is not even published as a compiled JAR, but rather still retains in the public repository as source code free to modify), or fix your class hierarchy to lift up the missing field because it can only work with fields on the same nest level.

internal var gson: Gson = GsonBuilder()
.registerTypeAdapterFactory(
RuntimeTypeAdapterFactory.of(BaseMessageListMetaDataItem::class.java, "viewHolderType")
.registerSubtype(TextWithImageMessageListMetaDataItem::class.java, "textWithImage")
.registerSubtype(TextOnlyMessageListMetaDataItem::class.java, "textOnly")
)
.create()

internal data class MessageListItem(
@field:SerializedName("meta_data")
val metaData: BaseMessageListMetaDataItem<*>?,
)

internal abstract class BaseMessageListMetaDataItem<out T>(
@field:SerializedName("viewHolderType")
val viewHolderType: String?,
@field:SerializedName("body")
val body: T?
) where T : BaseMessageListMetaDataItem.Body {

internal abstract class Body

}

internal class TextOnlyMessageListMetaDataItem
: BaseMessageListMetaDataItem<TextOnlyMessageListMetaDataItem.Body>(null, null) {

internal data class Body(
@field:SerializedName("title")
val title: String?
) : BaseMessageListMetaDataItem.Body()

}

internal class TextWithImageMessageListMetaDataItem
: BaseMessageListMetaDataItem<TextWithImageMessageListMetaDataItem.Body>(null, null) {

internal data class Body(
@field:SerializedName("title")
val title: String?,
@field:SerializedName("headerImage")
val headerImage: String?
) : BaseMessageListMetaDataItem.Body()

}


Related Topics



Leave a reply



Submit