Polymorphism with gson
Solution 1:
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.
Solution 2:
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 machine2
import 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);
}
}
Solution 3:
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>());
....
}
Solution 4:
GSON has a pretty good test case here showing how to define and register a type hierarchy adapter.
http://code.google.com/p/google-gson/source/browse/trunk/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java?r=739
To use that do this:
gson = new GsonBuilder()
.registerTypeAdapter(BaseQuestion.class, new BaseQuestionAdaptor())
.create();
Serialize method of the adapter can be a cascading if-else check of what type it is serializing.
JsonElement result = new JsonObject();
if (src instanceof SliderQuestion) {
result = context.serialize(src, SliderQuestion.class);
}
else if (src instanceof TextQuestion) {
result = context.serialize(src, TextQuestion.class);
}
else if (src instanceof ChoiceQuestion) {
result = context.serialize(src, ChoiceQuestion.class);
}
return result;
Deserializing is a bit hacky. In the unit test example, it checks for existence of tell-tale attributes to decide which class to deserialized to. If you can change the source of the object you're serializing, you can add a 'classType' attribute to each instance which holds the FQN of the instance class's name. This is so very un-object-oriented though.