Polimorfismo con gson

Tengo un problema deserializando una cadena json con Gson. Recibo una serie de comandos. El comando puede ser start, stop, algún otro tipo de comando. Naturalmente, tengo polymorphism y el comando de inicio / detención hereda del comando.

¿Cómo puedo serializarlo de nuevo al objeto de comando correcto usando gson?

Parece que obtengo solo el tipo base, que es el tipo declarado y nunca el tipo de tiempo de ejecución.

Esto es un poco tarde, pero hoy tuve que hacer exactamente lo mismo. Por lo tanto, en base a mi investigación y al usar gson-2.0, realmente no desea utilizar el método registerTypeHierarchyAdapter , sino más bien el registerTypeAdapter más mundano. Y, desde luego, no necesita hacer instancias ni escribir adaptadores para las clases derivadas: solo un adaptador para la clase base o la interfaz, siempre que esté satisfecho con la serialización predeterminada de las clases derivadas. De todos modos, aquí está el código (paquete e importaciones eliminados) (también disponible en github ):

La clase base (interfaz en mi caso):

public interface IAnimal { public String sound(); } 

Las dos clases derivadas, Cat:

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

Y perro:

 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 + ")"; } } 

IAnimalAdapter:

 public class IAnimalAdapter implements JsonSerializer, JsonDeserializer{ 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); } } 

Y la clase de prueba:

 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()); } } } 

Cuando ejecuta Test :: main, obtiene el siguiente resultado:

 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) 

De hecho, he hecho lo anterior utilizando también el método registerTypeHierarchyAdapter , pero eso parece requerir la implementación de clases de serializadores / deserializadores DogAdapter y CatAdapter personalizados, que son difíciles de mantener cada vez que desee agregar otro campo a Dog o Cat.

Actualmente, Gson tiene un mecanismo para registrar un adaptador tipo jerarquía que, según los informes, se puede configurar para una deserialización polimórfica simple, pero no veo cómo es ese el caso, ya que un adaptador tipo jerarquía parece ser un serializador / deserializador / creador de instancias combinado. dejando los detalles de la creación de instancia hasta el codificador, sin proporcionar ningún registro de tipo polimórfico real.

Parece que Gson pronto tendrá RuntimeTypeAdapter para una deserialización polimórfica más simple. Consulte http://code.google.com/p/google-gson/issues/detail?id=231 para obtener más información.

Si el uso del nuevo RuntimeTypeAdapter no es posible, y debe usar Gson, entonces creo que tendrá que implementar su propia solución, registrando un deserializador personalizado como un Adaptador de tipo jerarquía o como un Adaptador de tipo. El siguiente es uno de esos ejemplos.

 // 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 { String commandElementName; Gson gson; Map> commandRegistry; CommandDeserializer(String commandElementName) { this.commandElementName = commandElementName; GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); gson = gsonBuilder.create(); commandRegistry = new HashMap>(); } void registerCommand(String command, Class 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 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); } } 

GSON tiene un buen caso de prueba que muestra cómo definir y registrar un adaptador de jerarquía de tipo.

http://code.google.com/p/google-gson/source/browse/trunk/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java?r=739

Para usar eso haz esto:

  gson = new GsonBuilder() .registerTypeAdapter(BaseQuestion.class, new BaseQuestionAdaptor()) .create(); 

El método de serialización del adaptador puede ser una verificación en cascada if-else de qué tipo está serializando.

  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; 

Deserializar es un poco hacky. En el ejemplo de prueba unitaria, comprueba la existencia de atributos reveladores para decidir a qué clase deserializar. Si puede cambiar el origen del objeto que está serializando, puede agregar un atributo ‘classType’ a cada instancia que contenga el FQN del nombre de la clase de instancia. Sin embargo, esto es muy poco orientado a objetos.

Marcus Junius Brutus tuvo una gran respuesta (¡gracias!). Para ampliar su ejemplo, puede hacer que su clase de adaptador sea genérica para funcionar para todo tipo de objetos (no solo IAnimal) con los siguientes cambios:

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

Y en la clase de prueba:

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

Ha pasado mucho tiempo, pero no pude encontrar una solución realmente buena en línea. Aquí hay un pequeño giro en la solución de @ MarcusJuniusBrutus, que evita la recursión infinita.

Mantenga el mismo deserializador, pero elimine el serializador –

 public class IAnimalAdapter implements JsonDeSerializer { 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); } } 

Luego, en su clase original, agregue un campo con @SerializedName("CLASSNAME") . El truco ahora es inicializar esto en el constructor de la clase base , por lo que convierta su interfaz en una clase abstracta.

 public abstract class IAnimal { @SerializedName("CLASSNAME") public String className; public IAnimal(...) { ... className = this.getClass().getName(); } } 

La razón por la cual no hay una recursión infinita aquí es que pasamos la clase de tiempo de ejecución real (es decir, Dog not IAnimal) a context.deserialize . Esto no llamará a nuestro adaptador de tipo, siempre que usemos registerTypeAdapter y no registerTypeHierarchyAdapter

Google ha lanzado su propia RuntimeTypeAdapterFactory para manejar el polymorphism, pero desafortunadamente no es parte del núcleo gson (debe copiar y pegar la clase dentro de su proyecto).

Ejemplo:

 RuntimeTypeAdapterFactory runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory .of(Animal.class, "type") .registerSubtype(Dog.class, "dog") .registerSubtype(Cat.class, "cat"); Gson gson = new GsonBuilder() .registerTypeAdapterFactory(runtimeTypeAdapterFactory) .create(); 

Aquí he publicado un ejemplo completo de que funciona con el modelo Animal, Dog and Cat.

Creo que es mejor confiar en este adaptador en lugar de volver a implementarlo desde cero.

Si desea administrar un TypeAdapter para un tipo y otro para su subtipo, puede usar un TypeAdapterFactory como este:

 public class InheritanceTypeAdapterFactory implements TypeAdapterFactory { private Map, TypeAdapter> adapters = new LinkedHashMap<>(); { adapters.put(Animal.class, new AnimalTypeAdapter()); adapters.put(Dog.class, new DogTypeAdapter()); } @SuppressWarnings("unchecked") @Override public  TypeAdapter create(Gson gson, TypeToken typeToken) { TypeAdapter typeAdapter = null; Class currentType = Object.class; for (Class type : adapters.keySet()) { if (type.isAssignableFrom(typeToken.getRawType())) { if (currentType.isAssignableFrom(type)) { currentType = type; typeAdapter = (TypeAdapter)adapters.get(type); } } } return typeAdapter; } } 

Esta fábrica enviará el TypeAdapter más preciso