Polimorfismo com gson

Eu tenho um problema ao desserializar uma cadeia de json com o Gson. Eu recebo uma matriz de comandos. O comando pode ser iniciar, parar, algum outro tipo de comando. Naturalmente eu tenho polymorphism e o comando start / stop herda do comando.

Como posso serializá-lo de volta ao object de comando correto usando o gson?

Parece que eu recebo apenas o tipo base, que é o tipo declarado e nunca o tipo de tempo de execução.

   

Isso é um pouco tarde, mas eu tive que fazer exatamente a mesma coisa hoje. Portanto, com base em minha pesquisa e ao usar o gson-2.0, você não quer usar o método registerTypeHierarchyAdapter , mas sim o registerTypeAdapter mais comum. E você certamente não precisa fazer instanceofs ou escrever adaptadores para as classs derivadas: apenas um adaptador para a class ou interface base, desde que você esteja satisfeito com a serialização padrão das classs derivadas. De qualquer forma, aqui está o código (pacote e importações removidos) (também disponível no github ):

A class base (interface no meu caso):

public interface IAnimal { public String sound(); } 

As duas classs 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\""; }; } 

E cão:

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

O IAdimalAdapter:

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

E a class de teste:

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

Quando você executa o Test :: main, obtém a seguinte saída:

 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) 

Eu realmente fiz o acima usando o método registerTypeHierarchyAdapter também, mas isso parecia exigir a implementação de classs de serializador / desserializador DogAdapter e CatAdapter que são difíceis de manter sempre que você quiser adicionar outro campo a Dog ou a Cat.

Gson atualmente tem um mecanismo para registrar um Type Hierarchy Adapter que supostamente pode ser configurado para desserialização polimórfica simples, mas não vejo como é esse o caso, já que um Type Hierarchy Adapter parece ser apenas um serializador combinado / desserializador / instância criador, deixando os detalhes da criação da instância até o codificador, sem fornecer nenhum registro de tipo polimórfico real.

Parece que o Gson logo terá o RuntimeTypeAdapter para uma desserialização polimórfica mais simples. Consulte http://code.google.com/p/google-gson/issues/detail?id=231 para mais informações.

Se o uso do novo RuntimeTypeAdapter não for possível, e você tiver que usar o Gson, acho que terá que implementar sua própria solução, registrando um desserializador personalizado como um adaptador de hierarquia de tipo ou como adaptador de tipo. A seguir é um exemplo.

 // 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< ? 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); } } 

GSON tem um bom caso de teste aqui mostrando como definir e registrar um adaptador de hierarquia de tipos.

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

Para usar isso, faça isso:

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

O método Serialize do adaptador pode ser uma verificação em cascata if-else de que tipo está sendo serializado.

  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; 

Desserialização é um pouco hacky. No exemplo de teste de unidade, ele verifica a existência de atributos indicadores para decidir a qual class desserializar. Se você puder alterar a origem do object que está sendo serializado, poderá adicionar um atributo ‘classType’ a cada instância que contém o FQN do nome da class da instância. Isso é muito pouco orientado a objects.

Marcus Junius Brutus teve uma ótima resposta (obrigado!). Para estender seu exemplo, você pode tornar sua class de adaptador genérica para funcionar para todos os tipos de objects (não apenas IAnimal) com as seguintes alterações:

 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 .... } 

E na class de teste:

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

Muito tempo se passou, mas eu não consegui encontrar uma solução realmente boa online. Aqui está uma pequena mudança na solução @ MarcusJuniusBrutus, que evita a recursion infinita.

Mantenha o mesmo desserializador, mas remova o 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); } } 

Em seguida, na sua class original, adicione um campo com @SerializedName("CLASSNAME") . O truque é agora inicializar isto no construtor da class base , então faça sua interface em uma class abstrata.

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

A razão pela qual não há recursion infinita aqui é que passamos a class de tempo de execução real (isto é, Dog not IAnimal) para context.deserialize . Isso não chamará nosso adaptador de tipo, desde que usemos registerTypeAdapter e não registerTypeHierarchyAdapter

O Google lançou seu próprio RuntimeTypeAdapterFactory para lidar com o polymorphism, mas infelizmente não faz parte do núcleo do gson (você deve copiar e colar a class dentro do seu projeto).

Exemplo:

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

Aqui eu postei um exemplo completo de como funciona usando o modelo Animal, Dog and Cat.

Acho melhor confiar nesse adaptador em vez de reimplementá-lo do zero.

Se você quiser gerenciar um TypeAdapter para um tipo e outro para seu subtipo, você pode usar um 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 irá enviar o TypeAdapter mais preciso