4

I am trying to use GraphAdapterBuilder which is an extra to the GSON library to serialize an object with cyclic references. It works great for class but fails when trying to deserialize an interface.

To deserialize interface( which GSON doesn't do by default ) I am using PropertyBasedInterfaceMarshal or InterfaceAdapter. These are registered as custom type adapters for the interfaces.

When using ether above both fail to deserialize the interface as they are only passed the graph id like "0x4" as generated by GraphAdapterBuilder. This is passed as the JsonElement in the deserializer. Obviously there is nothing that can be done with this id from within the deserializer.

Shouldn't these be caught by the GraphAdapterBuilder instead of trying to be deserialized? I have not been able to get around this, is this a bug with GraphAdapterBuilder or is there a way to get around this?

Community
  • 1
  • 1
Jug6ernaut
  • 8,219
  • 2
  • 26
  • 26

1 Answers1

2

Ok, this is a (working) stub for a solution. It's too late in Italy, to make it nicer.

You need a delegate function like this

package com.google.gson.graph;

/**
 * @author Giacomo Tesio
 */
public interface GenericFunction<Domain, Codomain> {
    Codomain map(Domain domain);
}

a TypeAdapterFactory like this:

package com.google.gson.graph;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


/**
 * @author Giacomo Tesio
 */
public class InterfaceAdapterFactory  implements TypeAdapterFactory {

    final Map<String, GenericFunction<Gson, TypeAdapter<?>>> adapters;
    private final Class<?> commonInterface;
    public InterfaceAdapterFactory(Class<?> commonInterface, Class<?>[] concreteClasses)
    {
        this.commonInterface = commonInterface;
        this.adapters = new HashMap<String, GenericFunction<Gson, TypeAdapter<?>>>();
        final TypeAdapterFactory me = this;
        for(int i = 0; i < concreteClasses.length; ++i)
        {
            final Class<?> clazz = concreteClasses[i];
            this.adapters.put(clazz.getName(), new GenericFunction<Gson, TypeAdapter<?>>(){
                public TypeAdapter<?> map(Gson gson) {
                     TypeToken<?> type = TypeToken.get(clazz);
                     return gson.getDelegateAdapter(me, type);
                }
            });
        }
    }
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
        if(!this.commonInterface.isAssignableFrom(type.getRawType())
           && !this.commonInterface.equals(type.getRawType()))
        {
            return delegate;
        }
        final TypeToken<T> typeToken = type;
        final Gson globalGson = gson;
        return new TypeAdapter<T>() {
           public void write(JsonWriter out, T value) throws IOException {
             out.beginObject();
             out.name("@t");
             out.value(value.getClass().getName());
             out.name("@v");
             delegate.write(out, value);
             out.endObject();
           }
           @SuppressWarnings({"unchecked"})
           public T read(JsonReader in) throws IOException {
               JsonToken peekToken = in.peek();
               if(peekToken == JsonToken.NULL) {
                   in.nextNull();
                   return null;
               }

               in.beginObject();
               String dummy = in.nextName();
               String typeName = in.nextString();
               dummy = in.nextName();
               TypeAdapter<?> specificDelegate = adapters.get(typeName).map(globalGson);
               T result = (T)specificDelegate.read(in);
               in.endObject();
               return result;
           }
        };
    }

}

a pair of tests like these

public final class InterfaceAdapterFactoryTest extends TestCase {

    public void testInterfaceSerialization1(){
        SampleInterface first = new SampleImplementation1(10);
        SampleInterfaceContainer toSerialize = new SampleInterfaceContainer("container", first);

        GsonBuilder gsonBuilder = new GsonBuilder();

        new GraphAdapterBuilder()
            .addType(SampleInterfaceContainer.class)
            .addType(SampleImplementation1.class)
            .addType(SampleImplementation2.class)
            .registerOn(gsonBuilder);
        gsonBuilder.registerTypeAdapterFactory(new InterfaceAdapterFactory(
                SampleInterface.class, new Class<?>[] { SampleImplementation1.class, SampleImplementation2.class }
                ));
        Gson gson = gsonBuilder.create();

        String json = gson.toJson(toSerialize);
        System.out.println(json);
        SampleInterfaceContainer deserialized = gson.fromJson(json, SampleInterfaceContainer.class);

        assertNotNull(deserialized);
        assertEquals(toSerialize.getName(), deserialized.getName());
        assertEquals(toSerialize.getContent().getNumber(), deserialized.getContent().getNumber());
    }

    public void testInterfaceSerialization2(){
        SampleImplementation2 first = new SampleImplementation2(5, "test");
        SampleInterfaceContainer toSerialize = new SampleInterfaceContainer("container", first);
        first.Container = toSerialize;

        GsonBuilder gsonBuilder = new GsonBuilder();

        new GraphAdapterBuilder()
            .addType(SampleInterfaceContainer.class)
            .addType(SampleImplementation1.class)
            .addType(SampleImplementation2.class)
            .registerOn(gsonBuilder);
        gsonBuilder.registerTypeAdapterFactory(new InterfaceAdapterFactory(
                SampleInterface.class, new Class<?>[] { SampleImplementation1.class, SampleImplementation2.class }
                ));
        Gson gson = gsonBuilder.create();

        String json = gson.toJson(toSerialize);
        System.out.println(json);
        SampleInterfaceContainer deserialized = gson.fromJson(json, SampleInterfaceContainer.class);

        assertNotNull(deserialized);
        assertEquals(toSerialize.getName(), deserialized.getName());
        assertEquals(5, deserialized.getContent().getNumber());
        assertEquals("test", ((SampleImplementation2)deserialized.getContent()).getName());
        assertSame(deserialized, ((SampleImplementation2)deserialized.getContent()).Container);
    }
}

and some sample classes (to verify that the tests pass)

public class SampleInterfaceContainer {
    private SampleInterface content;
    private String name;
    public SampleInterfaceContainer(String name, SampleInterface content)
    {
        this.name = name;
        this.content = content;
    }

    public String getName()
    {
        return this.name;
    }

    public SampleInterface getContent()
    {
        return this.content;
    }
}
public interface SampleInterface {
    int getNumber();
}
public class SampleImplementation1 implements SampleInterface{
    private int number;
    public SampleImplementation1()
    {
        this.number = 0;
    }
    public SampleImplementation1(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
}

public class SampleImplementation2 implements SampleInterface{
    private  int number;
    private String name;
    public SampleInterfaceContainer Container;
    public SampleImplementation2()
    {
        this.number = 0;
        this.name = "";
    }
    public SampleImplementation2(int number, String name)
    {
        this.number = number;
        this.name = name;
    }
    public int getNumber()
    {
        return this.number;
    }
    public String getName()
    {
        return this.name;
    }
}

While this has been a quick&dirty hack, it works like a charme.

You just have to pay attention at the order of the operations during the initialization of GsonBuilder. You have to initialize and register the GraphAdapterBuilder first and only after register this factory.

It has been funny (if a bit tricky since I'm not a Java expert).

Giacomo Tesio
  • 7,144
  • 3
  • 31
  • 48