Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Gson serializes runtime type in list, not specified compile-time type?

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"}]
like image 336
user2517647 Avatar asked Aug 01 '15 00:08

user2517647


People also ask

How does GSON work in Java?

Gson is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used to convert a JSON string to an equivalent Java object.

What is GSON toJson in Java?

Gson is the main actor class of Google Gson library. It provides functionalities to convert Java objects to matching JSON constructs and vice versa. Gson is first constructed using GsonBuilder and then toJson(Object) or fromJson(String, Class) methods are used to read/write JSON constructs.

Why do we use GSON?

GSON has been successfully used in several android apps that are in Google Play today. The benefit you get with GSON is that object mapping can save the time spent writing code.

Is GSON open source?

Gson (also known as Google Gson) is an open-source Java library to serialize and deserialize Java objects to (and from) JSON.


1 Answers

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"}]
like image 139
durron597 Avatar answered Oct 09 '22 00:10

durron597