1

I use gson 2.8.6 and this is my code:

interface Foo {

    String getTest();
}

class FooImpl implements Foo {

    private String test;

    @Override
    public String getTest() {
        return this.test;
    }

    public void setTest(String test) {
        this.test = test;
    }

    @Override
    public String toString() {
        return "FooImpl{" + "test=" + test + '}';
    }
}

class Bar {

    private Foo foo;

    public Foo getFoo() {
        return foo;
    }

    public void setFoo(Foo foo) {
        this.foo = foo;
    }

    @Override
    public String toString() {
        return "Bar{" + "foo=" + foo + '}';
    }
}

class FooInstanceCreator implements InstanceCreator<Foo> {  

    @Override
    public Foo createInstance(Type type) {
        return new FooImpl();
    }
}

public class NewMain3 {

    public static void main(String[] args) {
        FooImpl foo = new FooImpl();
        foo.setTest("this is test");

        Bar bar = new Bar();
        bar.setFoo(foo);

        Gson gson = new GsonBuilder()
                    .serializeNulls()
                    .registerTypeAdapter(Foo.class, new FooInstanceCreator())
                    .create();

        //TO
        String to = gson.toJson(bar);
        System.out.println("TO:" + to);

        //FROM
        Bar from = gson.fromJson(to, Bar.class);
        System.out.println("FROM:" + from);
    }
}

And this is output:

TO:{"foo":{"test":"this is test"}}
FROM:Bar{foo=FooImpl{test=null}}

As you see test field is lost. Could anyone say how to fix it?

Pavel_K
  • 10,748
  • 13
  • 73
  • 186
  • Doesn't answer your question, but shows this is improper usage of Gson: `This interface is implemented to create instances of a class that does not define a no-args constructor. If you can modify the class, you should instead add a private, or public no-args constructor.` (unless your real code is not as simple as this example) – Dici Apr 13 '20 at 11:02
  • @Dici I was looking for answer how to work with interface in gson and they advise to use instance creator. – Pavel_K Apr 13 '20 at 11:04
  • @Dici By the way, if you don't `registerTypeAdapter(Foo.class, new FooInstanceCreator())` then you will get `Exception in thread "main" java.lang.RuntimeException: Unable to invoke no-args constructor for interface mavenproject5.Foo. Registering an InstanceCreator with Gson for this type may fix this problem.` – Pavel_K Apr 13 '20 at 11:11
  • Ah ok, maybe due to the interface indeed – Dici Apr 13 '20 at 11:25

2 Answers2

2

Running the debugger on your code, this is where your deserialization stops (link). Apparently, interfaces have no bound fields, so it just creates the instance and then moves on without setting anything.

You can fix it doing the following:

final RuntimeTypeAdapterFactory<Foo> typeFactory = RuntimeTypeAdapterFactory
      .of(Foo.class, "type")
      .registerSubtype(FooImpl.class);

Gson gson = new GsonBuilder()
       .serializeNulls()
       .registerTypeAdapterFactory(typeFactory)
       .create();

where RuntimeTypeAdapterFactory can be found here. I found this in this StackOverflow question by the way, not very deeply hidden. What this does is that it will write the class name of the concrete class in the JSON object in order to deserialize it later on:

TO:{"foo":{"type":"FooImpl","test":"this is test"}}
FROM:Bar{foo=FooImpl{test=this is test}}

I don't have any experience with Gson but it seems a little less nice than Jackson on this specific aspect.

Dici
  • 25,226
  • 7
  • 41
  • 82
2

I think the idea of InstanceCreator misleadingly contradicts with how ReflectiveTypeAdapterFactory works:

  • the former deals with classes that are effectively data bags, not interfaces (and this is told by an example in the interface JavaDoc);
  • whilst the latter, according to the source code, resolves class bound fields (not interfaces that do not declare fields) in advance applying re-mapping strategies for @SerializedName that raises some complexity.

It sounds counter-intuitive to some extent, but I don't think this is a design issue, it's rather a pitfall. What you can do here is creating a custom type adapter which borrows a sub-type type adapter for its super type:

public static <T1, T2 extends T1> TypeAdapterFactory bind(final TypeToken<T1> superTypeToken, final TypeToken<T2> subTypeToken) {
    return new TypeAdapterFactory() {
        @Override
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            if ( !typeToken.equals(superTypeToken) ) {
                return null;
            }
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) gson.getDelegateAdapter(this, subTypeToken);
            return typeAdapter;
        }
    };
}

And then register it in your Gson builder:

private static final TypeToken<Foo> fooTypeToken = new TypeToken<Foo>() {};
private static final TypeToken<FooImpl> fooImplTypeToken = new TypeToken<FooImpl>() {};

private static final Gson gson = new GsonBuilder()
        .serializeNulls()
        .registerTypeAdapterFactory(bind(fooTypeToken, fooImplTypeToken))
        .create();

This should make a perfect round-trip for you.

(The example provided by Dici is another use-case: it's a polymorphic type adapter that deserializes an object from a JSON subtree based on a JSON property that designates the type to instantiate. On the other hand, the bind method above can be considered a special "typeless" specialization for RuntimeTypeAdapterFactory.)