I have switched to
Google Gson after many years of using
org.json library for supporting JSON data interchange format in Java.
org.json is a lower-level library, so that you have to create
JSONObject,
JSONArray,
JSONString, ... and do other low-level work.
Gson simplifies this work. It provides simple
toJson() and
fromJson() methods to convert arbitrary Java objects to JSON and vice-versa, supports Java Generics, allows custom representations for objects, generates compact and readability JSON output and has many other goodies. I love it more and more. The using is simple. Assume, we have a class called
Circle.
public class Circle {
private int radius = 10;
private String backgroundColor = "#FF0000";
private String borderColor = "#000000";
private double scaleFactor = 0.5;
...
// getter / setter
}
Serialization (Java object --> JSON) can be done as follows:
Circle circle = new Circle();
Gson gson = new Gson();
String json = gson.toJson(circle);
==> json is
{
"radius": 10,
"backgroundColor": "#FF0000",
"borderColor": "#000000",
"scaleFactor": 0.5,
...
}
Deserialization (JSON --> Java object) is just one line of code:
Circle circle2 = gson.fromJson(json, Circle.class);
==> circle2 is the same as the circle above
Everything works like a charm. There is only one problem I have faced with abstract classes. Assume, we have an abstract class
AbstractElement and many other classes extending this one
public abstract class AbstractElement {
private String uuid;
// getter / setter
}
public class Circle extends AbstractElement {
...
}
public class Rectangle extends AbstractElement {
...
}
public class Ellipse extends AbstractElement {
...
}
Assume now, we store all concrete classes in a list or a map parametrized with
AbstractElement
public class Whiteboard
{
private Map<String, AbstractElement> elements =
new LinkedHashMap<String, AbstractElement>();
...
}
The problem is that the concrete class is undisclosed during deserialization. It's unknown in the JSON representation of Whiteboard. How the right Java class should be instantiated from the JSON representation and put into the
Map<String, AbstractElement> elements? I have nothing found in the documentation what would address this problem. It is obvious that we need to store a meta information in JSON representations about concrete classes. That's for sure.
Gson allows you to register your own custom serializers and deserializers. That's a power feature of
Gson. Sometimes default representation is not what you want. This is often the case e.g. when dealing with third-party library classes. There are enough examples of how to write custom serializers / deserializers. I'm going to create an adapter class implementing both interfaces
JsonSerializer,
JsonDeserializer and to register it for my abstract class
AbstractElement.
GsonBuilder gsonBilder = new GsonBuilder();
gsonBilder.registerTypeAdapter(AbstractElement.class, new AbstractElementAdapter());
Gson gson = gsonBilder.create();
And here is
AbstractElementAdapter:
package com.googlecode.whiteboard.json;
import com.google.gson.*;
import com.googlecode.whiteboard.model.base.AbstractElement;
import java.lang.reflect.Type;
public class AbstractElementAdapter implements JsonSerializer<AbstractElement>, JsonDeserializer<AbstractElement> {
@Override
public JsonElement serialize(AbstractElement src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("type", new JsonPrimitive(src.getClass().getSimpleName()));
result.add("properties", context.serialize(src, src.getClass()));
return result;
}
@Override
public AbstractElement deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String type = jsonObject.get("type").getAsString();
JsonElement element = jsonObject.get("properties");
try {
return context.deserialize(element, Class.forName("com.googlecode.whiteboard.model." + type));
} catch (ClassNotFoundException cnfe) {
throw new JsonParseException("Unknown element type: " + type, cnfe);
}
}
}
I add two JSON properties - one is "
type" and the other is "
properties". The first property holds a concrete implementation class (simple name) of the
AbstractElement and the second one holds the serialized object itself. The JSON looks like
{
"type": "Circle",
"properties": {
"radius": 10,
"backgroundColor": "#FF0000",
"borderColor": "#000000",
"scaleFactor": 0.5,
...
}
}
We benefit from the "
type" property during deserialization. The concrete class can be instantiated now by
Class.forName("com.googlecode.whiteboard.model." + type) where
"com.googlecode.whiteboard.model." + type is a fully qualified class name. The following call
public <T> T deserialize(JsonElement json, Type typeOfT) throws JsonParseException
from
JsonDeserializationContext invokes default deserialization on the specified object and completes the job.
Wow ! Thanks , that saved me a lot of time !
ReplyDeleteWhen using registerTypeAdapter the serializer is never called for the concrete sub classes. When using registerTypeHierarchyAdapter the serializer hangs in a circular loop through context.serialize(src, src.getClass()));
ReplyDelete>> When using registerTypeAdapter the serializer is never called for the concrete sub classes. <<
ReplyDeleteFound out that it works if you hand TypeToken> when serializing the map.
I didn't implement your Whiteclass setup, probably it runs without TypeToken then.
Great post, very handy. And it works as a charm. Thanks.
ReplyDeleteHi Oleg, nice post - however i think there is a similar concept in .NET and some other serializers. They use an attribute called $type (and a few others) to store this kind of meta information. Also why not include the "full" class name - this will make it easier to create your instances when deserializing.
ReplyDeletewdorninger
It's up to you whether to include the full class name. I don't and instead do Class.forName(AbstractElement.class.getPackage() + "." + type). That way, if the package name ever changes, your deserializer still works.
DeleteThank you very much. It's good sample.
ReplyDeleteVery productive post. Works as expected :)
ReplyDeleteExcelent post!!! Thank you for the kindess of posting this solution!! Saved me!!
ReplyDeleteIf you previously used a different strategy (e.g. there is no "properties" and "type" field in the json) you can simply deserialize the whole object to your class.
ReplyDeleteSuch as:
if(jsonObject.has("properties"){
doAsInThisTutorial();
}else{
return context.deserialize(json,YourClass.class);
}
Just thought Id drop this here since I am sure Im not the only one refactoring in a live project :)