Gson serializa una lista de objetos polimórficos

Estoy tratando de serializar / deserializar un objeto, que involucra polymorphism, en JSON usando Gson.

Este es mi código para serializar:

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

Este es el resultado:

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

La serialización funciona principalmente, excepto que falta el contenido de los miembros heredados (En particular obix:BatchIn y obixBatchout faltan). Aquí está mi clase 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; } ... } 

Así es como se ve mi clase heredada (ObixOp):

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

Me doy cuenta de que podría usar un adaptador para esto, pero el problema es que estoy serializando una colección del tipo de clase base ObixBaseObj . Hay alrededor de 25 clases que heredan de esto. ¿Cómo puedo hacer que esto funcione elegantemente?

Creo que un serializador / deserializador personalizado es la única forma de proceder y traté de proponerle la forma más compacta de realizarlo que he encontrado. Me disculpo por no usar sus clases, pero la idea es la misma (solo quería al menos 1 clase base y 2 clases ampliadas).

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

y ahora este es el código que ejecuté para probar todo:

 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 es mi ejecución:

 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] 

Algunas explicaciones: el truco lo realiza otro Gson dentro del serializador / deserializador. Solo uso isA field para detectar la clase correcta. Para ir más rápido, uso un mapa para asociar la cadena isA a la clase correspondiente. Luego, hago la serialización / deserialización adecuada usando el segundo objeto Gson. Lo declare como estático, por lo que no ralentizará la serialización / deserialización con la asignación múltiple de Gson.

Pro. En realidad, no escribes código con más código que este, permites que Gson haga todo el trabajo. Simplemente tiene que recordar poner una nueva subclase en los mapas (la excepción lo recuerda).

Contras usted tiene dos mapas. Creo que mi implementación puede refinarse un poco para evitar duplicaciones de mapas, pero se los dejé a usted (o al futuro editor, si corresponde).

Tal vez desee unificar la serialización y la deserialización en un objeto único, debe comprobar la clase TypeAdapter o experimentar con un objeto que implemente ambas interfaces.

Hay una solución simple: Gson’s RuntimeTypeAdapterFactory . No tiene que escribir ningún serializador, esta clase funciona para usted. Prueba esto con tu 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)); } 

Salida:

 {"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": [] } ] } 

EDITAR: mejor ejemplo de trabajo.

Dijiste que hay alrededor de 25 clases que heredan de ObixBaseObj .

Comenzamos a escribir una nueva clase, 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(); } 

Cada vez que necesitamos un objeto Gson , en lugar de llamar al new Gson() , llamaremos

 GsonUtils.getGson() 

Agregamos este código a 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 cada vez que se ObixBaseObj una instancia de esta clase o una clase para niños de ObixBaseObj , la clase se registrará en RuntimeTypeAdapter

En las clases para niños, solo se necesita un cambio mínimo:

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

Ejemplo de trabajo:

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

Salida:

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

Espero que ayude.

Aprecio las otras respuestas aquí que me llevaron en mi camino para resolver este problema. RuntimeTypeAdapterFactory una combinación de RuntimeTypeAdapterFactory con Reflection .

También creé una clase de ayuda para asegurarme de que se usara un Gson configurado correctamente.

Dentro de un bloque estático dentro de la clase GsonHelper, tengo el siguiente código para acceder a mi proyecto y buscar y registrar todos los tipos apropiados. Todos mis objetos que pasarán por JSON-ification son un subtipo de Jsonable. Deseará cambiar lo siguiente:

  1. my.project en Reflections debería ser tu nombre de paquete.
  2. Jsonable.class es mi clase base. Sustituye el tuyo
  3. Me gusta que el campo muestre el nombre canónico completo, pero claramente, si no lo quiere / necesita, puede omitir esa parte de la llamada para registrar el subtipo. Lo mismo ocurre con className en RuntimeAdapterFactory ; Tengo elementos de datos que ya usan el campo 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(); }