8

Why does it seem that Gson ignores the nested generic type declaration when serializing?

I am trying to get Gson to use the compile-time type I specify, instead of the runtime type of objects in the list. I am also using an abstract superclass for A.java, but the example below has the same problem.

public class A {
    public String foo;
}

public class B extends A {
    public String bar;
}

public static void main( String[] args ) {
    Gson gson = new Gson();

    B b = new B();
    b.foo = "foo";
    b.bar = "bar";

    List<A> list = new ArrayList<A>();
    list.add(b);

    System.out.println(gson.toJson(b, new TypeToken<A>(){}.getType()));
    System.out.println(gson.toJson(b, new TypeToken<B>(){}.getType()));

    System.out.println(gson.toJson(list, new TypeToken<List<A>>(){}.getType()));
    System.out.println(gson.toJson(list, new TypeToken<List<B>>(){}.getType()));
}

Output:

{"foo":"foo"}
{"bar":"bar","foo":"foo"}
[{"bar":"bar","foo":"foo"}]
[{"bar":"bar","foo":"foo"}]

Expected:

{"foo":"foo"}
{"bar":"bar","foo":"foo"}
[{"foo":"foo"}]
[{"bar":"bar","foo":"foo"}]
1
  • 2
    Welcome to Stack Overflow! I just want you to know that this is an excellent question for your first post here. I hope you participate more in the future!
    – durron597
    Commented Aug 1, 2015 at 17:38

1 Answer 1

4

This is because of how Gson serializes collections by default.

Why does this happen?

Scroll to the bottom if you don't care about why and just want a fix.

Gson's default CollectionTypeAdapterFactory wraps it's element type adapters in something called a TypeAdapterRuntimeTypeWrapper. When choosing the proper adapter, it uses the following priorities:

// Order of preference for choosing type adapters
// First preference: a type adapter registered for the runtime type
// Second preference: a type adapter registered for the declared type
// Third preference: reflective type adapter for the runtime type (if it is a sub class of the declared type)
// Fourth preference: reflective type adapter for the declared type

In this case, the Third preference is an adapter for B, and the Fourth preference is an adapter for A. This is not avoidable when using the default serializers, as there's no conditional in the CollectionTypeAdapterFactory:

public Adapter(Gson context, Type elementType,
    TypeAdapter<E> elementTypeAdapter,
    ObjectConstructor<? extends Collection<E>> constructor) {
  this.elementTypeAdapter =
      new TypeAdapterRuntimeTypeWrapper<E>(context, elementTypeAdapter, elementType);
  this.constructor = constructor;
}

This wrapper is not present when not using the CollectionTypeAdapterFactory, which is why it doesn't happen in your first two examples.

tl;dr Okay so how do I fix it?

The only way to get around this problem is to register a custom serializer. Writing one for A will do the trick, in your use case:

public class ATypeAdapter extends TypeAdapter<A> {
  public A read(JsonReader reader) throws IOException {
    if (reader.peek() == JsonToken.NULL) {
      reader.nextNull();
      return null;
    }
    reader.beginObject();
    String name = reader.nextName();
    if(!"foo".equals(name)) throw new JsonSyntaxException("Expected field named foo");
    A a = new A();
    a.foo = reader.nextString();
    reader.endObject();
    return a;
  }

  public void write(JsonWriter writer, A value) throws IOException {
    if (value == null) {
      writer.nullValue();
      return;
    }
    writer.beginObject();
    writer.name("foo");
    writer.value(value.foo);
    writer.endObject();
  }
}

Then, if you do:

public static void main( String[] args ) {
    GsonBuilder builder = new GsonBuilder();
    builder.registerTypeAdapter(new TypeToken<A>(){}.getType(), new ATypeAdapter());
    Gson gson = builder.create();

    B b = new B();
    b.foo = "foo";
    b.bar = "bar";

    List<A> list = new ArrayList<A>();
    list.add(b);

    System.out.println(gson.toJson(b, new TypeToken<A>(){}.getType()));
    System.out.println(gson.toJson(b, new TypeToken<B>(){}.getType()));

    System.out.println(gson.toJson(list, new TypeToken<List<A>>(){}.getType()));
    System.out.println(gson.toJson(list, new TypeToken<List<B>>(){}.getType()));
}

You get the expected output:

{"foo":"foo"}
{"bar":"bar","foo":"foo"}
[{"foo":"foo"}]
[{"bar":"bar","foo":"foo"}]

Not the answer you're looking for? Browse other questions tagged or ask your own question.