Polimorfismo con gonorrea

Ho un problema nella deserializzazione di una stringa json con Gson. Ricevo una serie di comandi. Il comando può essere start, stop, qualche altro tipo di comando. Naturalmente ho il polimorfismo e il comando start / stop eredita dal comando.

Come posso serializzarlo di nuovo sull’object comando corretto usando gson?

Sembra che ottenga solo il tipo base, cioè il tipo dichiarato e mai il tipo runtime.

È un po ‘tardi, ma oggi ho dovuto fare esattamente la stessa cosa. Quindi, basandomi sulle mie ricerche e quando si utilizza gson-2.0, non si vuole usare il metodo registerTypeHierarchyAdapter , ma piuttosto il registerTypeAdapter più banale. E certamente non hai bisogno di fare instanceofs o scrivere adattatori per le classi derivate: solo un adattatore per la class base o l’interfaccia, a patto ovviamente che tu sia soddisfatto della serializzazione predefinita delle classi derivate. Ad ogni modo, ecco il codice (pacchetto e importazioni rimossi) (disponibile anche in github ):

La class base (interfaccia nel mio caso):

public interface IAnimal { public String sound(); } 

Le due classi derivate, 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 cane:

 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, 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 la class di test:

 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 esegui Test :: main ottieni il seguente risultato:

 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) 

In realtà ho fatto quanto sopra usando il metodo registerTypeHierarchyAdapter , ma sembrava richiedere l’implementazione di classi personalizzate di serializzatore / deserializzatore DogAdapter e CatAdapter, il che fa fatica a mantenere ogni volta che si desidera aggiungere un altro campo a Dog o a Cat.

Gson attualmente ha un meccanismo per registrare un Type Hierarchy Adapter che, a quanto si dice, può essere configurato per una semplice deserializzazione polimorfica, ma non vedo come sia, dato che un Type Hierarchy Adapter sembra essere un serializzatore / deserializzatore / creatore di istanze combinato, lasciando i dettagli della creazione dell’istanza al codificatore, senza fornire alcuna registrazione di tipo polimorfico effettivo.

Sembra che Gson abbia presto RuntimeTypeAdapter per una deserializzazione polimorfica più semplice. Vedi http://code.google.com/p/google-gson/issues/detail?id=231 per maggiori informazioni.

Se l’uso del nuovo RuntimeTypeAdapter non è ansible, e devi usare Gson, allora penso che dovrai eseguire il rollover della tua soluzione, registrando un deserializzatore personalizzato sia come Type Hierarchy Adapter che come Type Adapter. Di seguito è riportato un esempio.

 // 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 ha un buon test case qui che mostra come definire e registrare un adattatore di gerarchia di tipi.

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

Per usare quello fai questo:

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

Il metodo Serialize dell’adattatore può essere un controllo ifay else a cascata di quale tipo sta serializzando.

  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; 

La deserializzazione è un po ‘hacky. Nell’esempio del test unitario, verifica l’esistenza degli attributi rivelatori per decidere a quale class deserializzare. Se è ansible modificare l’origine dell’object che si sta serializzando, è ansible aggiungere un attributo ‘classType’ a ogni istanza che contiene l’FQN del nome della class di istanza. Questo è così molto poco orientato agli oggetti.

Marcus Junius Brutus ha avuto una grande risposta (grazie!). Per estendere il suo esempio, puoi rendere la sua class adapter generica per tutti i tipi di oggetti (non solo IAnimal) con le seguenti modifiche:

 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 nella class di test:

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

È passato molto tempo, ma non sono riuscito a trovare una soluzione davvero valida online. Ecco una piccola svolta sulla soluzione di @ MarcusJuniusBrutus, che evita l’infinita ricorsione.

Mantieni lo stesso deserializzatore, ma rimuovi il serializzatore –

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

Quindi, nella class originale, aggiungi un campo con @SerializedName("CLASSNAME") . Il trucco ora è inizializzarlo nel costruttore della class base , quindi rendi la tua interfaccia una class astratta.

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

Il motivo per cui non esiste una ricorsione infinita qui è che passiamo la class di esecuzione effettiva (cioè Dog not IAnimal) a context.deserialize . Questo non chiamerà il nostro tipo di adattatore, a patto che usiamo registerTypeAdapter e non registerTypeHierarchyAdapter

Google ha rilasciato il proprio RuntimeTypeAdapterFactory per gestire il polimorfismo, ma sfortunatamente non fa parte del nucleo gson (è necessario copiare e incollare la class all’interno del progetto).

Esempio:

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

Qui ho pubblicato un esempio completo di esso utilizzando il modello Animal, Dog and Cat.

Penso che sia meglio fare affidamento su questo adattatore piuttosto che reimplementarlo da zero.

Se vuoi gestire un TypeAdapter per un tipo e un altro per il suo sottotipo, puoi usare TypeAdapterFactory in questo modo:

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

Questo stabilimento invierà il TypeAdapter più preciso