Gson serializa uma lista de objects polimórficos

Eu estou tentando serializar / desserializar um object, que envolve polymorphism, em JSON usando o Gson.

Este é o meu código para serialização:

ObixBaseObj lobbyObj = new ObixBaseObj(); lobbyObj.setIs("obix:Lobby"); ObixOp batchOp = new ObixOp(); batchOp.setName("batch"); batchOp.setIn("obix:BatchIn"); batchOp.setOut("obix:BatchOut"); lobbyObj.addChild(batchOp); Gson gson = new Gson(); System.out.println(gson.toJson(lobbyObj)); 

Aqui está o resultado:

  {"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch"}]} 

A serialização funciona principalmente, exceto pela falta do conteúdo de membros herdados (Em particular, as obix:BatchIn e obixBatchout estão faltando). Aqui está minha class base:

 public class ObixBaseObj { protected String obix; private String display; private String displayName; private ArrayList children; public ObixBaseObj() { obix = "obj"; } public void setName(String name) { this.name = name; } ... } 

Aqui está como minha class herdada (ObixOp) se parece:

 public class ObixOp extends ObixBaseObj { private String in; private String out; public ObixOp() { obix = "op"; } public ObixOp(String in, String out) { obix = "op"; this.in = in; this.out = out; } public String getIn() { return in; } public void setIn(String in) { this.in = in; } public String getOut() { return out; } public void setOut(String out) { this.out = out; } } 

Eu percebo que eu poderia usar um adaptador para isso, mas o problema é que eu estou serializando uma coleção de tipo de class base ObixBaseObj . Existem cerca de 25 classs que herdam disso. Como posso fazer isso funcionar elegantemente?

Eu acho que um serializador personalizado / desserializador é a única maneira de prosseguir e eu tentei propor-lhe a maneira mais compacta para perceber que eu encontrei. Peço desculpas por não usar suas aulas, mas a idéia é a mesma (eu só queria pelo menos uma class base e duas classs estendidas).

BaseClass.java

 public class BaseClass{ @Override public String toString() { return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]"; } public ArrayList list = new ArrayList(); protected String isA="BaseClass"; public int x; } 

ExtendedClass1.java

 public class ExtendedClass1 extends BaseClass{ @Override public String toString() { return "ExtendedClass1 [total=" + total + ", number=" + number + ", list=" + list + ", isA=" + isA + ", x=" + x + "]"; } public ExtendedClass1(){ isA = "ExtendedClass1"; } public Long total; public Long number; } 

ExtendedClass2.java

 public class ExtendedClass2 extends BaseClass{ @Override public String toString() { return "ExtendedClass2 [total=" + total + ", list=" + list + ", isA=" + isA + ", x=" + x + "]"; } public ExtendedClass2(){ isA = "ExtendedClass2"; } public Long total; } 

CustomDeserializer.java

 public class CustomDeserializer implements JsonDeserializer> { private static Map map = new TreeMap(); static { map.put("BaseClass", BaseClass.class); map.put("ExtendedClass1", ExtendedClass1.class); map.put("ExtendedClass2", ExtendedClass2.class); } public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { List list = new ArrayList(); JsonArray ja = json.getAsJsonArray(); for (JsonElement je : ja) { String type = je.getAsJsonObject().get("isA").getAsString(); Class c = map.get(type); if (c == null) throw new RuntimeException("Unknow class: " + type); list.add(context.deserialize(je, c)); } return list; } } 

CustomSerializer.java

 public class CustomSerializer implements JsonSerializer> { private static Map map = new TreeMap(); static { map.put("BaseClass", BaseClass.class); map.put("ExtendedClass1", ExtendedClass1.class); map.put("ExtendedClass2", ExtendedClass2.class); } @Override public JsonElement serialize(ArrayList src, Type typeOfSrc, JsonSerializationContext context) { if (src == null) return null; else { JsonArray ja = new JsonArray(); for (BaseClass bc : src) { Class c = map.get(bc.isA); if (c == null) throw new RuntimeException("Unknow class: " + bc.isA); ja.add(context.serialize(bc, c)); } return ja; } } } 

e agora este é o código que eu executei para testar a coisa toda:

 public static void main(String[] args) { BaseClass c1 = new BaseClass(); ExtendedClass1 e1 = new ExtendedClass1(); e1.total = 100L; e1.number = 5L; ExtendedClass2 e2 = new ExtendedClass2(); e2.total = 200L; e2.x = 5; BaseClass c2 = new BaseClass(); c1.list.add(e1); c1.list.add(e2); c1.list.add(c2); List al = new ArrayList(); // this is the instance of BaseClass before serialization System.out.println(c1); GsonBuilder gb = new GsonBuilder(); gb.registerTypeAdapter(al.getClass(), new CustomDeserializer()); gb.registerTypeAdapter(al.getClass(), new CustomSerializer()); Gson gson = gb.create(); String json = gson.toJson(c1); // this is the corresponding json System.out.println(json); BaseClass newC1 = gson.fromJson(json, BaseClass.class); System.out.println(newC1); } 

Esta é minha execução:

 BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0] {"list":[{"total":100,"number":5,"list":[],"isA":"ExtendedClass1","x":0},{"total":200,"list":[],"isA":"ExtendedClass2","x":5},{"list":[],"isA":"BaseClass","x":0}],"isA":"BaseClass","x":0} BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0] 

Algumas explicações: o truque é feito por outro Gson dentro do serializador / desserializador. Eu uso apenas um campo para identificar a class certa. Para ir mais rápido, eu uso um mapa para associar a string isA à class correspondente. Em seguida, faço a serialização / desserialização adequada usando o segundo object Gson. Eu declarei como estático, para que você não diminua a serialização / desserialização com a alocação múltipla do Gson.

Pro Você realmente não escreve código mais código do que isso, você deixa Gson fazer todo o trabalho. Você só precisa lembrar de colocar uma nova subclass nos mapas (a exceção lembra você disso).

Contras Você tem dois mapas. Eu acho que minha implementação pode refinar um pouco para evitar duplicações de mapas, mas deixei para você (ou para o futuro editor, se houver).

Talvez você queira unificar a serialização e a desserialização em um object único, você deve verificar a class TypeAdapter ou experimentar um object que implemente ambas as interfaces.

Há uma solução simples: o RuntimeTypeAdapterFactory do Gson . Você não precisa escrever nenhum serializador, essa class faz todo o trabalho para você. Tente isso com seu código:

  ObixBaseObj lobbyObj = new ObixBaseObj(); lobbyObj.setIs("obix:Lobby"); ObixOp batchOp = new ObixOp(); batchOp.setName("batch"); batchOp.setIn("obix:BatchIn"); batchOp.setOut("obix:BatchOut"); lobbyObj.addChild(batchOp); RuntimeTypeAdapterFactory adapter = RuntimeTypeAdapterFactory .of(ObixBaseObj.class) .registerSubtype(ObixBaseObj.class) .registerSubtype(ObixOp.class); Gson gson2=new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create(); Gson gson = new Gson(); System.out.println(gson.toJson(lobbyObj)); System.out.println("---------------------"); System.out.println(gson2.toJson(lobbyObj)); } 

Saída:

 {"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch","children":[]}]} --------------------- { "type": "ObixBaseObj", "obix": "obj", "is": "obix:Lobby", "children": [ { "type": "ObixOp", "in": "obix:BatchIn", "out": "obix:BatchOut", "obix": "op", "name": "batch", "children": [] } ] } 

EDIT: Melhor exemplo de trabalho.

Você disse que existem cerca de 25 classs que são ObixBaseObj do ObixBaseObj .

Começamos a escrever uma nova aula, GsonUtils

 public class GsonUtils { private static final GsonBuilder gsonBuilder = new GsonBuilder() .setPrettyPrinting(); public static void registerType( RuntimeTypeAdapterFactory adapter) { gsonBuilder.registerTypeAdapterFactory(adapter); } public static Gson getGson() { return gsonBuilder.create(); } 

Toda vez que precisarmos de um object Gson , ao invés de new Gson() , chamaremos

 GsonUtils.getGson() 

Adicionamos esse código ao ObixBaseObj:

 public class ObixBaseObj { protected String obix; private String display; private String displayName; private String name; private String is; private ArrayList children = new ArrayList(); // new code private static final RuntimeTypeAdapterFactory adapter = RuntimeTypeAdapterFactory.of(ObixBaseObj.class); private static final HashSet> registeredClasses= new HashSet>(); static { GsonUtils.registerType(adapter); } private synchronized void registerClass() { if (!registeredClasses.contains(this.getClass())) { registeredClasses.add(this.getClass()); adapter.registerSubtype(this.getClass()); } } public ObixBaseObj() { registerClass(); obix = "obj"; } 

Por quê? porque toda vez que esta class ou uma class ObixBaseObj do ObixBaseObj for instanciada, a class será registrada no RuntimeTypeAdapter

Nas classs filhas, apenas uma alteração mínima é necessária:

 public class ObixOp extends ObixBaseObj { private String in; private String out; public ObixOp() { super(); obix = "op"; } public ObixOp(String in, String out) { super(); obix = "op"; this.in = in; this.out = out; } 

Exemplo de trabalho:

 public static void main(String[] args) { ObixBaseObj lobbyObj = new ObixBaseObj(); lobbyObj.setIs("obix:Lobby"); ObixOp batchOp = new ObixOp(); batchOp.setName("batch"); batchOp.setIn("obix:BatchIn"); batchOp.setOut("obix:BatchOut"); lobbyObj.addChild(batchOp); Gson gson = GsonUtils.getGson(); System.out.println(gson.toJson(lobbyObj)); } 

Saída:

 { "type": "ObixBaseObj", "obix": "obj", "is": "obix:Lobby", "children": [ { "type": "ObixOp", "in": "obix:BatchIn", "out": "obix:BatchOut", "obix": "op", "name": "batch", "children": [] } ] } 

Espero que ajude.

Eu aprecio as outras respostas aqui que me levaram no meu caminho para resolver este problema. Eu usei uma combinação de RuntimeTypeAdapterFactory com o Reflection .

Eu também criei uma class auxiliar para garantir que um Gson corretamente configurado fosse usado.

Dentro de um bloco estático dentro da class GsonHelper, eu tenho o seguinte código percorrendo meu projeto para encontrar e registrar todos os tipos apropriados. Todos os meus objects que passarão por JSON-ification são um subtipo de Jsonable. Você desejará alterar o seguinte:

  1. my.project in Reflections deve ser o nome do seu pacote.
  2. Jsonable.class é minha class base. Substitua o seu.
  3. Eu gosto de ter o campo mostrando o nome canônico completo, mas claramente, se você não quer / precisa, você pode deixar de fora a parte da chamada para registrar o subtipo. O mesmo vale para className no RuntimeAdapterFactory ; Eu tenho itens de dados já usando o campo de type .

     private static final GsonBuilder gsonBuilder = new GsonBuilder() .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .excludeFieldsWithoutExposeAnnotation() .setPrettyPrinting(); static { Reflections reflections = new Reflections("my.project"); Set> allTypes = reflections.getSubTypesOf(Jsonable.class); for (Class< ? extends Jsonable> serClass : allTypes){ Set subTypes = reflections.getSubTypesOf(serClass); if (subTypes.size() > 0){ RuntimeTypeAdapterFactory adapterFactory = RuntimeTypeAdapterFactory.of(serClass, "className"); for (Object o : subTypes ){ Class c = (Class)o; adapterFactory.registerSubtype(c, c.getCanonicalName()); } gsonBuilder.registerTypeAdapterFactory(adapterFactory); } } } public static Gson getGson() { return gsonBuilder.create(); }