Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gson serialize a list of polymorphic objects

I'm trying to serialize/deserialize an object, that involves polymorphism, into JSON using Gson.

This is my code for serializing:

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

Here's the result:

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

The serialization mostly works, except its missing the contents of inherited members (In particular obix:BatchIn and obixBatchout strings are missing). Here's my base class:

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

Here's what my inherited class (ObixOp) looks like:

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

I realize I could use an adapter for this, but the problem is that I'm serializing a collection of base class type ObixBaseObj. There are about 25 classes that inherits from this. How can I make this work elegantly?

like image 485
l46kok Avatar asked Oct 25 '13 11:10

l46kok


People also ask

How do you serialize with Gson?

Serialization in the context of Gson means converting a Java object to its JSON representation. In order to do the serialization, we need to create the Gson object, which handles the conversion. Next, we need to call the function toJson() and pass the User object. Program output.

Is Gson better than Jackson?

ConclusionBoth Gson and Jackson are good options for serializing/deserializing JSON data, simple to use and well documented. Advantages of Gson: Simplicity of toJson/fromJson in the simple cases. For deserialization, do not need access to the Java entities.


2 Answers

There's a simple solution: Gson's RuntimeTypeAdapterFactory (from com.google.code.gson:gson-extras:$gsonVersion). You don't have to write any serializer, this class does all work for you. Try this with your code:

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

Output:

{"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: Better working example.

You said that there are about 25 classes that inherits from ObixBaseObj.

We start writing a new class, 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();     } 

Every time we need a Gson object, instead of calling new Gson(), we will call

GsonUtils.getGson() 

We add this code to ObixBaseObj:

public class ObixBaseObj {     protected String obix;     private String display;     private String displayName;     private String name;     private String is;     private ArrayList<ObixBaseObj> children = new ArrayList<ObixBaseObj>();     // new code     private static final RuntimeTypeAdapterFactory<ObixBaseObj> adapter =              RuntimeTypeAdapterFactory.of(ObixBaseObj.class);      private static final HashSet<Class<?>> registeredClasses= new HashSet<Class<?>>();      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";     } 

Why? because every time this class or a children class of ObixBaseObj is instantiated, the class it's gonna be registered in the RuntimeTypeAdapter

In the child classes, only a minimal change is needed:

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

Working example:

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

Output:

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

I hope it helps.

like image 71
rpax Avatar answered Oct 13 '22 11:10

rpax


I think that a custom serializer/deserializer is the only way to proceed and I tried to propose you the most compact way to realize it I have found. I apologize for not using your classes, but the idea is the same (I just wanted at least 1 base class and 2 extended classes).

BaseClass.java

public class BaseClass{          @Override     public String toString() {         return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]";     }          public ArrayList<BaseClass> list = new ArrayList<BaseClass>();          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<List<BaseClass>> {      private static Map<String, Class> map = new TreeMap<String, Class>();      static {         map.put("BaseClass", BaseClass.class);         map.put("ExtendedClass1", ExtendedClass1.class);         map.put("ExtendedClass2", ExtendedClass2.class);     }      public List<BaseClass> deserialize(JsonElement json, Type typeOfT,             JsonDeserializationContext context) throws JsonParseException {          List list = new ArrayList<BaseClass>();         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<ArrayList<BaseClass>> {      private static Map<String, Class> map = new TreeMap<String, Class>();      static {         map.put("BaseClass", BaseClass.class);         map.put("ExtendedClass1", ExtendedClass1.class);         map.put("ExtendedClass2", ExtendedClass2.class);     }      @Override     public JsonElement serialize(ArrayList<BaseClass> 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;         }     } } 

and now this is the code I executed to test the whole thing:

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<BaseClass> al = new ArrayList<BaseClass>();    // 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);  } 

This is my execution:

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] 

Some explanations: the trick is done by another Gson inside the serializer/deserializer. I use just isA field to spot the right class. To go faster, I use a map to associate the isA string to the corresponding class. Then, I do the proper serialization/deserialization using the second Gson object. I declared it as static so you won't slow serialization/deserialization with multiple allocation of Gson.

Pro You actually do not write more code than this, you let Gson do all the work. You have just to remember to put a new subclass into the maps (the exception reminds you of that).

Cons You have two maps. I think that my implementation can refined a bit to avoid map duplications, but I left them to you (or to future editor, if any).

Maybe you want to unify serialization and deserialization into a unique object, you should be check the TypeAdapter class or experiment with an object that implements both interfaces.

like image 23
giampaolo Avatar answered Oct 13 '22 11:10

giampaolo