Skip to content

[bug] sub class field missing during serialization if collection's element type is a WildcardType #1870

Closed

Description

@cnzhujie

I am serializing a list like this:

List<? extends TestBeanBase> list1;
list1.add(new TestBeanSub());    // TestBeanSub is a child class of TestBeanBase

I find that the fields of TestBeanSub is missed in the serialized json.

You can try my example code:

    @Test
    public void testList() {
        List<TestBeanBase> list = new ArrayList<>();

        //base class
        TestBeanBase baseBean = new TestBeanBase();
        baseBean.a = "1";
        list.add(baseBean);

        //sub class
        TestBeanSub subBean = new TestBeanSub();
        subBean.a = "1";
        subBean.b = "2";//b is a field declared in sub class, this field value is missed during serialization
        list.add(subBean);

        TestBeanList beanList = new TestBeanList();
        beanList.list1 = list;

        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        System.out.println(gson.toJson(beanList));
    }

    public static class TestBeanList {
        public List<? extends TestBeanBase> list1; //element type is a WildcardType
    }

    public static class TestBeanBase {
        public String a;
    }

    public static class TestBeanSub extends TestBeanBase {
        public String b;
    }

Output json :

{
  "list1": [
    {
      "a": "1"
    },
    {
      "a": "1"   //field b is missed
    }
  ]
}

Activity

  1. Marcono1234 commented on Mar 3, 2021

    @Marcono1234
    Contributor

    There is #170 which seems to exactly describe your problem, but it was merged into #231 despite that issue apparently covering a completely different use case.
    Edit: No, that issue is probably unrelated because it does not involve usage of wildcards. Maybe that issue is from before TypeAdapterRuntimeTypeWrapper (or similar functionality) existed.

    The underlying issue is that TypeAdapterRuntimeTypeWrapper.getRuntimeTypeIfMoreSpecific(Type, Object) (internal Gson method) does not choose the runtime type when a wildcard is used (which might be a bug?).

    A workaround for this would be to create a custom TypeAdapterFactory which always uses the adapter for the runtime type:

    class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
        public static final RuntimeTypeAdapterFactory INSTANCE = new RuntimeTypeAdapterFactory();
        
        private RuntimeTypeAdapterFactory() { }
        
        @Override
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
            
            return new TypeAdapter<T>() {
                @Override
                public T read(JsonReader in) throws IOException {
                    return delegate.read(in);
                }
                
                @Override
                public void write(JsonWriter out, T value) throws IOException {
                    if (value == null) {
                        // Let compile-time type adapter handle `null`
                        delegate.write(out, null);
                    }
                    else {
                        @SuppressWarnings("unchecked")
                        TypeToken<T> runtimeType = (TypeToken<T>) TypeToken.get(value.getClass());
                        // Get adapter for runtime type
                        TypeAdapter<T> delegate = gson.getDelegateAdapter(RuntimeTypeAdapterFactory.this, runtimeType);
                        delegate.write(out, value);
                    }
                }
            };
        }
    }

    This factory would then have to be registered after all other factories and adapters on the GsonBuilder:

    Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.INSTANCE)
        .create();

    This will decrease performance and might have unintended side effects when you actually want to use the adapter for the compile-time type in some cases.

  2. cnzhujie commented on Mar 9, 2021

    @cnzhujie
    Author

    There is #170 which seems to exactly describe your problem, but it was merged into #231 despite that issue apparently covering a completely different use case.

    The underlying issue is that TypeAdapterRuntimeTypeWrapper.getRuntimeTypeIfMoreSpecific(Type, Object) (internal Gson method) does not choose the runtime type when a wildcard is used (which might be a bug?).

    A workaround for this would be to create a custom TypeAdapterFactory which always uses the adapter for the runtime type:

    class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
        public static final RuntimeTypeAdapterFactory INSTANCE = new RuntimeTypeAdapterFactory();
        
        private RuntimeTypeAdapterFactory() { }
        
        @Override
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
            
            return new TypeAdapter<T>() {
                @Override
                public T read(JsonReader in) throws IOException {
                    return delegate.read(in);
                }
                
                @Override
                public void write(JsonWriter out, T value) throws IOException {
                    if (value == null) {
                        // Let compile-time type adapter handle `null`
                        delegate.write(out, null);
                    }
                    else {
                        @SuppressWarnings("unchecked")
                        TypeToken<T> runtimeType = (TypeToken<T>) TypeToken.get(value.getClass());
                        // Get adapter for runtime type
                        TypeAdapter<T> delegate = gson.getDelegateAdapter(RuntimeTypeAdapterFactory.this, runtimeType);
                        delegate.write(out, value);
                    }
                }
            };
        }
    }

    This factory would then have to be registered after all other factories and adapters on the GsonBuilder:

    Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.INSTANCE)
        .create();

    This will decrease performance and might have unintended side effects when you actually want to use the adapter for the compile-time type in some cases.

    Thanks, it works for me

  3. added a commit that references this issue on Sep 5, 2026
    5843050
  4. added a commit that references this issue on Sep 16, 2026
    698ba9e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions