Gson serialize a list of polymorphic objects

2019-01-02 20:38发布

问题:

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?

回答1:

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 code 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 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 unificate serialization and deserialization into a unique object, you should be check the TypeAdapter class or experiment with an object that implements both interfaces.



回答2:

There's a simple solution: Gson's RuntimeTypeAdapterFactory . 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.



回答3:

I appreciate the other answers here that led me on my path to solving this issue. I used a combination of RuntimeTypeAdapterFactory with Reflection.

I also created a helper class to make sure a properly configured Gson was used.

Within a static block inside the GsonHelper class, I have the following code go through my project to find and register all of the appropriate types. All of my objects that will go through JSON-ification are a subtype of Jsonable. You will want to change the following:

  1. my.project in Reflections should be your package name.
  2. Jsonable.class is my base class. Substitute yours.
  3. I like having the field show the full canonical name, but clearly if you don't want / need it, you can leave out that part of the call to register the subtype. The same thing goes for className in the RuntimeAdapterFactory; I have data items already using the type field.

    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<Class<? extends Jsonable>> 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();
    }
    


标签: