Search code examples
jsonrestserializationgsoncircular-reference

How to solve circular reference when serializing an object which have a class member with the same type of that object


I'm facing this issue when using Gson to serialize an object which has a class member with the same type:

https://github.com/google/gson/issues/1447

The object:

public class StructId implements Serializable {
private static final long serialVersionUID = 1L;

public String Name;
public StructType Type;
public StructId ParentId;
public StructId ChildId;

And since StructId contains ParentId/ChildId with the same type I was getting infinite loop when trying to serialize it, so what I did is:

private Gson gson = new GsonBuilder()
.setExclusionStrategies(new ExclusionStrategy() {

        public boolean shouldSkipClass(Class<?> clazz) {
            return false; //(clazz == StructId.class);
        }

        /**
          * Custom field exclusion goes here
          */
        public boolean shouldSkipField(FieldAttributes f) {
            //Ignore inner StructIds to solve circular serialization
            return ( f.getName().equals("ParentId") || f.getName().equals("ChildId") ); 
        }

     })
    /**
      * Use serializeNulls method if you want To serialize null values 
      * By default, Gson does not serialize null values
      */
    .serializeNulls()
    .create();

But this is not good enough cause I need the data inside Parent/Child and ignoring them while serializing is not a solution. How is it possible to solve it?

Related to the answer marked as Solution:

I have such a struct: - Struct1 -- Table --- Variable1

The object before serialization is: enter image description here

And Json that is generated is: enter image description here

As you can see, the ParentId of Table is "Struct1" but the ChildId of "Struct1" is empty and it should be "Table"

B.R.


Solution

  • I think using ExclusionStrategy is not the right approach to solve this problem.

    I would rather suggest to use JsonSerializer and JsonDeserializer customized for your StructId class.
    (May be an approach using TypeAdapter would be even better, but I didn't have enough Gson experience do get this working.)

    So you would create your Gson instance by:

    Gson gson = new GsonBuilder()
        .registerTypeAdapter(StructId.class, new StructIdSerializer())
        .registerTypeAdapter(StructId.class, new StructIdDeserializer())
        .setPrettyPrinting()
        .create();
    

    The StructIdSerializer class below is responsible for converting a StructId to JSON. It converts its properties Name, Type and ChildId to JSON. Note that it does not convert the property ParentId to JSON, because doing that would produce infinite recursion.

    public class StructIdSerializer implements JsonSerializer<StructId> {
    
        @Override
        public JsonElement serialize(StructId src, Type typeOfSrc, JsonSerializationContext context) {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("Name", src.Name);
            jsonObject.add("Type", context.serialize(src.Type));
            jsonObject.add("ChildId", context.serialize(src.ChildId));  // recursion!
            return jsonObject;
        }
    }
    

    The StructIdDeserializer class below is responsible for converting JSON to a StructId. It converts the JSON properties Name, Type and ChildId to corresponding Java fields in StructId. Note that the ParentId Java field is reconstructed from the JSON nesting structure, because it is not directly contained as a JSON property.

    public class StructIdDeserializer implements JsonDeserializer<StructId> {
    
        @Override
        public StructId deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
            throws JsonParseException {
            StructId id = new StructId();
            id.Name = json.getAsJsonObject().get("Name").getAsString();
            id.Type = context.deserialize(json.getAsJsonObject().get("Type"), StructType.class);
            JsonElement childJson = json.getAsJsonObject().get("ChildId");
            if (childJson != null) {
                id.ChildId = context.deserialize(childJson, StructId.class);  // recursion!
                id.ChildId.ParentId = id;
            }
            return id;
        }
    }
    

    I tested the code above with this JSON input example

    {
        "Name": "John",
        "Type": "A",
        "ChildId": {
            "Name": "Jane",
            "Type": "B",
            "ChildId": {
                "Name": "Joe",
                "Type": "A"
            }
        }
    }
    

    by deserializing it with
    StructId root = gson.fromJson(new FileReader("example.json"), StructId.class);,
    then by serializing that with
    System.out.println(gson.toJson(root));
    and got the original JSON again.