Polymorphism with gson
I have a problem deserializing a json string with Gson. I receive an array of commands. The command can be start, stop , some other type of command. Naturally I have polymorphism, and start/stop command inherit from command.
How can I serialize it back to the correct command object using gson?
Seems that I get only the base type, that is the declare开发者_JS百科d type and never the runtime type.
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 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);
}
}
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>());
....
}
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.
Google has released its own RuntimeTypeAdapterFactory to handle the polymorphism but unfortunately it's not part of the gson core (you must copy and paste the class inside your project).
Example:
RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(runtimeTypeAdapterFactory)
.create();
Here I have posted a full working example of it using the models Animal, Dog and Cat.
I think it's better to rely on this adapter rather than reimplement it from scratch.
Long time has passed, but I couldn't find a really good solution online.. Here is small twist on @MarcusJuniusBrutus's solution, that avoids the infinite recursion.
Keep the same deserializer, but remove the serializer -
public class IAnimalAdapter implements JsonDeSerializer<IAnimal> {
private static final String CLASSNAME = "CLASSNAME";
private static final String INSTANCE = "INSTANCE";
@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);
}
}
Then, in your original class, add a field with @SerializedName("CLASSNAME")
.
The trick is now to initialize this in the constructor of the base class, so make your interface into a an abstract class.
public abstract class IAnimal {
@SerializedName("CLASSNAME")
public String className;
public IAnimal(...) {
...
className = this.getClass().getName();
}
}
The reason there is no infinite recursion here is that we pass the actual runtime class (i.e. Dog not IAnimal) to context.deserialize
. This will not call our type adapter, as long as we use registerTypeAdapter
and not registerTypeHierarchyAdapter
Updated Answer - Best parts of all other answers
I am describing solutions for various use cases and would be addressing the infinite recursion problem as well
Case 1: You are in control of the classes, i.e, you get to write your own
Cat
,Dog
classes as well as theIAnimal
interface. You can simply follow the solution provided by @marcus-junius-brutus(the top rated answer)There won't be any infinite recursion if there is a common base interface as
IAnimal
But, what if I don't want to implement the
IAnimal
or any such interface?Then, @marcus-junius-brutus(the top rated answer) will produce an infinite recursion error. In this case, we can do something like below.
We would have to create a copy constructor inside the base class and a wrapper subclass as follows:
.
// Base class(modified)
public class Cat implements IAnimal {
public String name;
public Cat(String name) {
super();
this.name = name;
}
// COPY CONSTRUCTOR
public Cat(Cat cat) {
this.name = cat.name;
}
@Override
public String sound() {
return name + " : \"meaow\"";
};
}
// The wrapper subclass for serialization
public class CatWrapper extends Cat{
public CatWrapper(String name) {
super(name);
}
public CatWrapper(Cat cat) {
super(cat);
}
}
And the serializer for the type Cat
:
public class CatSerializer implements JsonSerializer<Cat> {
@Override
public JsonElement serialize(Cat src, Type typeOfSrc, JsonSerializationContext context) {
// Essentially the same as the type Cat
JsonElement catWrapped = context.serialize(new CatWrapper(src));
// Here, we can customize the generated JSON from the wrapper as we want.
// We can add a field, remove a field, etc.
return modifyJSON(catWrapped);
}
private JsonElement modifyJSON(JsonElement base){
// TODO: Modify something
return base;
}
}
So, why a copy constructor?
Well, once you define the copy constructor, no matter how much the base class changes, your wrapper will continue with the same role. Secondly, if we don't define a copy constructor and simply subclass the base class then we would have to "talk" in terms of the extended class, i.e, CatWrapper
. It is quite possible that your components talk in terms of the base class and not the wrapper type.
Is there an easy alternative?
Sure, it has now been introduced by Google - this is the RuntimeTypeAdapterFactory
implementation:
RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(runtimeTypeAdapterFactory)
.create();
Here, you would need to introduce a field called "type" in Animal
and the value of the same inside Dog
to be "dog", Cat
to be "cat"
Complete example: https://static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html
Case 2: You are not in control of the classes. You join a company or use a library where the classes are already defined and your manager doesn't want you to change them in any way - You can subclass your classes and have them implement a common marker interface(which doesn't have any methods) such as
AnimalInterface
.Ex:
.
// The class we are NOT allowed to modify
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 marker interface
public interface AnimalInterface {
}
// The subclass for serialization
public class DogWrapper extends Dog implements AnimalInterface{
public DogWrapper(String name, int ferocity) {
super(name, ferocity);
}
}
// The subclass for serialization
public class CatWrapper extends Cat implements AnimalInterface{
public CatWrapper(String name) {
super(name);
}
}
So, we would be using CatWrapper
instead of Cat
, DogWrapper
instead of Dog
and
AlternativeAnimalAdapter
instead of IAnimalAdapter
// The only difference between `IAnimalAdapter` and `AlternativeAnimalAdapter` is that of the interface, i.e, `AnimalInterface` instead of `IAnimal`
public class AlternativeAnimalAdapter implements JsonSerializer<AnimalInterface>, JsonDeserializer<AnimalInterface> {
private static final String CLASSNAME = "CLASSNAME";
private static final String INSTANCE = "INSTANCE";
@Override
public JsonElement serialize(AnimalInterface 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 AnimalInterface 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);
}
}
We perform a test:
public class Test {
public static void main(String[] args) {
// Note that we are using the extended classes instead of the base ones
IAnimal animals[] = new IAnimal[]{new CatWrapper("Kitty"), new DogWrapper("Brutus", 5)};
Gson gsonExt = null;
{
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(AnimalInterface.class, new AlternativeAnimalAdapter());
gsonExt = builder.create();
}
for (IAnimal animal : animals) {
String animalJson = gsonExt.toJson(animal, AnimalInterface.class);
System.out.println("serialized with the custom serializer:" + animalJson);
AnimalInterface animal2 = gsonExt.fromJson(animalJson, AnimalInterface.class);
}
}
}
Output:
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.CatWrapper","INSTANCE":{"name":"Kitty"}}
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.DogWrapper","INSTANCE":{"name":"Brutus","ferocity":5}}
If you want manage a TypeAdapter for a type and an other for his sub type, you can use a TypeAdapterFactory like this :
public class InheritanceTypeAdapterFactory implements TypeAdapterFactory {
private Map<Class<?>, TypeAdapter<?>> adapters = new LinkedHashMap<>();
{
adapters.put(Animal.class, new AnimalTypeAdapter());
adapters.put(Dog.class, new DogTypeAdapter());
}
@SuppressWarnings("unchecked")
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
TypeAdapter<T> typeAdapter = null;
Class<?> currentType = Object.class;
for (Class<?> type : adapters.keySet()) {
if (type.isAssignableFrom(typeToken.getRawType())) {
if (currentType.isAssignableFrom(type)) {
currentType = type;
typeAdapter = (TypeAdapter<T>)adapters.get(type);
}
}
}
return typeAdapter;
}
}
This factory will send the most accurate TypeAdapter
If you combine Marcus Junius Brutus's answer with user2242263's edit, you can avoid having to specify a large class hierarchy in your adapter by defining your adapter as working on an interface type. You can then provide default implementations of toJSON() and fromJSON() in your interface (which only includes these two methods) and have every class you need to serialize implement your interface. To deal with casting, in your subclasses you can provide a static fromJSON() method that deserializes and performs the appropriate casting from your interface type. This worked superbly for me (just be careful about serializing/deserializing classes that contain hashmaps--add this when you instantiate your gson builder:
GsonBuilder builder = new GsonBuilder().enableComplexMapKeySerialization();
Hope this helps someone save some time and effort!
精彩评论