Gson Serialize Circular References Using Stubs

603 views Asked by At

I'm trying to implement some simple Json serialization functionality but I'm having a hard time coping with the massive complexity of Gson.

So basically I have a bunch of Entity classes which reference each other with a lot of circular reference. To serialize this structure to JSON I want to keep track of the objects already serialized. The Entity classes all implement an interface called Identified which has one method String getId() giving a globally unique id. So during serializiation of one root element, I want to store all encountered ids in a Set and decide based on that set, whether to fully serialize an object or to serialize that object as a stub

"something": { "__stub": "true", "id": "..." }

This shouldn't be too hard a task in my opinion, but I haven't been able to put something together. Using a custom JsonSerializer I'm not able to have an object (that is not to be serialized as a stub) serialized in the default way. Using a TypeAdapterFactory, I'm not able to access the actual object.

So, any help on how to achieve this, would be very nice!

Best regards

1

There are 1 answers

0
Lyubomyr Shaydariv On

I'm not sure if it's possible easily. As far as I know, Gson promotes immutability and seems to lack custom serialization context support (at least I don't know if it's possible to use custom JsonSerializationContext wherever possible). Thus, one of possible work-around might be the following:

IIdentifiable.java

A simple interface to request a custom ID for an object.

interface IIdentifiable<ID> {

    ID getId();

}

Entity.java

A simple entity that can hold another entity references in two manners:

  • a direct dependency to a "next" entity;
  • a collection of references to other references.
final class Entity
        implements IIdentifiable<String> {

    @SerializedName(ID_PROPERTY_NAME)
    private final String id;

    private final Collection<Entity> entities = new ArrayList<>();
    private Entity next;

    private Entity(final String id) {
        this.id = id;
    }

    static Entity entity(final String id) {
        return new Entity(id);
    }

    @Override
    public String getId() {
        return id;
    }

    Entity setAll(final Entity... entities) {
        this.entities.clear();
        this.entities.addAll(asList(entities));
        return this;
    }

    Entity setNext(final Entity next) {
        this.next = next;
        return this;
    }

}

IdentitySerializingTypeAdapterFactory.java

I didn't find any easier way rather than making it a type adapter factory, and, unfortunately, this implementation is totally stateful and cannot be reused.

final class IdentitySerializingTypeAdapterFactory
        implements TypeAdapterFactory {

    private final Collection<Object> traversedEntityIds = new HashSet<>();

    private IdentitySerializingTypeAdapterFactory() {
    }

    static TypeAdapterFactory identitySerializingTypeAdapterFactory() {
        return new IdentitySerializingTypeAdapterFactory();
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        final boolean isIdentifiable = IIdentifiable.class.isAssignableFrom(typeToken.getRawType());
        final TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, typeToken);
        if ( isIdentifiable ) {
            return new TypeAdapter<T>() {
                @Override
                public void write(final JsonWriter out, final T value)
                        throws IOException {
                    final IIdentifiable<?> identifiable = (IIdentifiable<?>) value;
                    final Object id = identifiable.getId();
                    if ( !traversedEntityIds.contains(id) ) {
                        delegateAdapter.write(out, value);
                        traversedEntityIds.add(id);
                    } else {
                        out.beginObject();
                        out.name(REF_ID_PROPERTY_NAME);
                        writeSimpleValue(out, id);
                        out.endObject();
                    }
                }

                @Override
                public T read(final JsonReader in) {
                    throw new UnsupportedOperationException();
                }
            };
        }
        return delegateAdapter;
    }

}

The type adapter firstly tries to check if a given entity has been already traversed. If yes, then it's writing a special object similar to your one (the behavior could be rewritten via the strategy pattern, of course, but let it be more simple). If no, then the default type adapter is obtained, and then the given entity is delegated to that adapter, and registered as a traversed one if the latter type adapter succeeds.

The rest

And here is the rest.

SystemNames.java

final class SystemNames {

    private SystemNames() {
    }

    private static final String SYSTEM_PREFIX = "__$";

    static final String ID_PROPERTY_NAME = SYSTEM_PREFIX + "id";
    static final String REF_ID_PROPERTY_NAME = SYSTEM_PREFIX + "refId";

}

GsonJsonWriters.java

final class GsonJsonWriters {

    private GsonJsonWriters() {
    }

    static void writeSimpleValue(final JsonWriter writer, final Object value)
            throws IOException {
        if ( value == null ) {
            writer.nullValue();
        } else if ( value instanceof Double ) {
            writer.value((double) value);
        } else if ( value instanceof Long ) {
            writer.value((long) value);
        } else if ( value instanceof String ) {
            writer.value((String) value);
        } else if ( value instanceof Boolean ) {
            writer.value((Boolean) value);
        } else if ( value instanceof Number ) {
            writer.value((Number) value);
        } else {
            throw new IllegalArgumentException("Cannot handle values of type " + value);
        }
    }

}

Testing

In the test below, there are three entities identified by FOO, BAR, and BAZ string identifiers. All of them have circular dependencies like this:

  • FOO -> BAR, BAR -> BAZ, BAZ -> FOO using the next property;
  • FOO -> [BAR, BAZ], BAR -> [FOO, BAZ], BAZ -> [FOO, BAR] using the entities property.

Since the type adapter factory is stateful, even GsonBuilder must be created from scratch thus not having "spoiled" state between use. Simply speaking, once a Gson instance is used once, it must be disposed, so there are GsonBuilder suppliers in the test below.

public final class Q41213747Test {

    private static final Entity foo = entity("FOO");
    private static final Entity bar = entity("BAR");
    private static final Entity baz = entity("BAZ");

    static {
        foo.setAll(bar, baz).setNext(bar);
        bar.setAll(foo, baz).setNext(baz);
        baz.setAll(foo, bar).setNext(foo);
    }

    @Test
    public void testSerializeSameJson() {
        final String json1 = newSerializingGson().toJson(foo);
        final String json2 = newSerializingGson().toJson(foo);
        assertThat("Must be the same between the calls because the GSON instances are stateful", json1, is(json2));
    }

    @Test
    public void testSerializeNotSameJson() {
        final Gson gson = newSerializingGson();
        final String json1 = gson.toJson(foo);
        final String json2 = gson.toJson(foo);
        assertThat("Must not be the same between the calls because the GSON instance is stateful", json1, is(not(json2)));
    }

    @Test
    public void testOutput() {
        out.println(newSerializingGson().toJson(foo));
    }

    private static Gson newSerializingGson() {
        return newSerializingGson(GsonBuilder::new);
    }

    private static Gson newSerializingGson(final Supplier<GsonBuilder> defaultGsonBuilderSupplier) {
        return defaultGsonBuilderSupplier.get()
                .registerTypeAdapterFactory(identitySerializingTypeAdapterFactory())
                .create();
    }

}
{
    "__$id": "FOO",
    "entities": [
        {
            "__$id": "BAR",
            "entities": [
                {
                    "__$refId": "FOO"
                },
                {
                    "__$id": "BAZ",
                    "entities": [
                        {
                            "__$refId": "FOO"
                        },
                        {
                            "__$refId": "BAR"
                        }
                    ],
                    "next": {
                        "__$refId": "FOO"
                    }
                }
            ],
            "next": {
                "__$refId": "BAZ"
            }
        },
        {
            "__$refId": "BAZ"
        }
    ],
    "next": {
        "__$refId": "BAR"
    }
}

Deserialization of such stuff looks really complicated. At least using GSON facilities.


Do you consider rethinking your JSON model in order to avoid circular dependencies in JSON output? Maybe decomposing your objects to a single map like Map<ID, Object> and making references transient or @Expose-annotated could be easier for you to use? It would simplify deserialization as well.