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?
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With