Polymorphism with gson Polymorphism with gson json json

Polymorphism with gson


This is a bit late but I had to do exactly the same thing today. So, based on my research and when using gson-2.0 you really don't want to use the registerTypeHierarchyAdapter method, but rather the more mundane registerTypeAdapter. And you certainly don't need to do instanceofs or write adapters for the derived classes: just one adapter for the base class or interface, provided of course that you are happy with the default serialization of the derived classes. Anyway, here's the code (package and imports removed) (also available in github):

The base class (interface in my case):

public interface IAnimal { public String sound(); }

The two derived classes, Cat:

public class Cat implements IAnimal {    public String name;    public Cat(String name) {        super();        this.name = name;    }    @Override    public String sound() {        return name + " : \"meaow\"";    };}

And Dog:

public class Dog implements IAnimal {    public String name;    public int ferocity;    public Dog(String name, int ferocity) {        super();        this.name = name;        this.ferocity = ferocity;    }    @Override    public String sound() {        return name + " : \"bark\" (ferocity level:" + ferocity + ")";    }}

The IAnimalAdapter:

public class IAnimalAdapter implements JsonSerializer<IAnimal>, JsonDeserializer<IAnimal>{    private static final String CLASSNAME = "CLASSNAME";    private static final String INSTANCE  = "INSTANCE";    @Override    public JsonElement serialize(IAnimal src, Type typeOfSrc,            JsonSerializationContext context) {        JsonObject retValue = new JsonObject();        String className = src.getClass().getName();        retValue.addProperty(CLASSNAME, className);        JsonElement elem = context.serialize(src);         retValue.add(INSTANCE, elem);        return retValue;    }    @Override    public IAnimal deserialize(JsonElement json, Type typeOfT,            JsonDeserializationContext context) throws JsonParseException  {        JsonObject jsonObject = json.getAsJsonObject();        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);        String className = prim.getAsString();        Class<?> klass = null;        try {            klass = Class.forName(className);        } catch (ClassNotFoundException e) {            e.printStackTrace();            throw new JsonParseException(e.getMessage());        }        return context.deserialize(jsonObject.get(INSTANCE), klass);    }}

And the Test class:

public class Test {    public static void main(String[] args) {        IAnimal animals[] = new IAnimal[]{new Cat("Kitty"), new Dog("Brutus", 5)};        Gson gsonExt = null;        {            GsonBuilder builder = new GsonBuilder();            builder.registerTypeAdapter(IAnimal.class, new IAnimalAdapter());            gsonExt = builder.create();        }        for (IAnimal animal : animals) {            String animalJson = gsonExt.toJson(animal, IAnimal.class);            System.out.println("serialized with the custom serializer:" + animalJson);            IAnimal animal2 = gsonExt.fromJson(animalJson, IAnimal.class);            System.out.println(animal2.sound());        }    }}

When you run the Test::main you get the following output:

serialized with the custom serializer:{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Cat","INSTANCE":{"name":"Kitty"}}Kitty : "meaow"serialized with the custom serializer:{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Dog","INSTANCE":{"name":"Brutus","ferocity":5}}Brutus : "bark" (ferocity level:5)

I've actually done the above using the registerTypeHierarchyAdapter method too, but that seemed to require implementing custom DogAdapter and CatAdapter serializer/deserializer classes which are a pain to maintain any time you want to add another field to Dog or to Cat.


Gson currently has a mechanism to register a Type Hierarchy Adapter that reportedly can be configured for simple polymorphic deserialization, but I don't see how that's the case, as a Type Hierarchy Adapter appears to just be a combined serializer/deserializer/instance creator, leaving the details of instance creation up to the coder, without providing any actual polymorphic type registration.

It looks like Gson will soon have the RuntimeTypeAdapter for simpler polymorphic deserialization. See http://code.google.com/p/google-gson/issues/detail?id=231 for more info.

If use of the new RuntimeTypeAdapter isn't possible, and you gotta use Gson, then I think you'll have to roll your own solution, registering a custom deserializer either as a Type Hierarchy Adapter or as Type Adapter. Following is one such example.

// output://     Starting machine1//     Stopping machine2import java.lang.reflect.Type;import java.util.HashMap;import java.util.Map;import com.google.gson.FieldNamingPolicy;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;public class Foo{  // [{"machine_name":"machine1","command":"start"},{"machine_name":"machine2","command":"stop"}]  static String jsonInput = "[{\"machine_name\":\"machine1\",\"command\":\"start\"},{\"machine_name\":\"machine2\",\"command\":\"stop\"}]";  public static void main(String[] args)  {    GsonBuilder gsonBuilder = new GsonBuilder();    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);    CommandDeserializer deserializer = new CommandDeserializer("command");    deserializer.registerCommand("start", Start.class);    deserializer.registerCommand("stop", Stop.class);    gsonBuilder.registerTypeAdapter(Command.class, deserializer);    Gson gson = gsonBuilder.create();    Command[] commands = gson.fromJson(jsonInput, Command[].class);    for (Command command : commands)    {      command.execute();    }  }}class CommandDeserializer implements JsonDeserializer<Command>{  String commandElementName;  Gson gson;  Map<String, Class<? extends Command>> commandRegistry;  CommandDeserializer(String commandElementName)  {    this.commandElementName = commandElementName;    GsonBuilder gsonBuilder = new GsonBuilder();    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);    gson = gsonBuilder.create();    commandRegistry = new HashMap<String, Class<? extends Command>>();  }  void registerCommand(String command, Class<? extends Command> commandInstanceClass)  {    commandRegistry.put(command, commandInstanceClass);  }  @Override  public Command deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)      throws JsonParseException  {    try    {      JsonObject commandObject = json.getAsJsonObject();      JsonElement commandTypeElement = commandObject.get(commandElementName);      Class<? extends Command> commandInstanceClass = commandRegistry.get(commandTypeElement.getAsString());      Command command = gson.fromJson(json, commandInstanceClass);      return command;    }    catch (Exception e)    {      throw new RuntimeException(e);    }  }}abstract class Command{  String machineName;  Command(String machineName)  {    this.machineName = machineName;  }  abstract void execute();}class Stop extends Command{  Stop(String machineName)  {    super(machineName);  }  void execute()  {    System.out.println("Stopping " + machineName);  }}class Start extends Command{  Start(String machineName)  {    super(machineName);  }  void execute()  {    System.out.println("Starting " + machineName);  }}


Marcus Junius Brutus had a great answer (thanks!). To extend his example, you can make his adapter class generic to work for all types of objects (Not just IAnimal) with the following changes:

class InheritanceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T>{....    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)....    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException....}

And in the Test Class:

public class Test {    public static void main(String[] args) {        ....            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());        ....}