1

Consider this example:

static class BaseBean { String baseField = "base"; }
static class ChildBean extends BaseBean { String childField = "child"; }

static class BaseBeanHolder {
    List <? extends BaseBean> beans;

    public BaseBeanHolder(List<? extends BaseBean> beans) { this.beans = beans; }
}

static class ChildBeanHolder {
    List <ChildBean> beans;

    public ChildBeanHolder(List<ChildBean> beans) { this.beans = beans; }
}

@Test
public void mcve() {
    BaseBeanHolder baseHolder = new BaseBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(baseHolder));

    ChildBeanHolder childHolder = new ChildBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(childHolder));
}

It prints:

{"beans":[{"baseField":"base"}]}

{"beans":[{"childField":"child","baseField":"base"}]}

So, although both lists hold child objects, only the second holder results in the child fields being serialized to JSON.

I have seen other questions, like here but I wondering whether there are reasonable workarounds to achieve what I want.

In other words: is there a way to have such one "holder" class that accepts either BaseBeans or ChildBeans (the <? extends BaseBean> does that), and that also gives me the correct results when serialising instances with Gson into JSON strings?

( note: I can't use specific type adapters, as I have no control where that actual Gson instance is coming from and how it is configured in our environment )

Community
  • 1
  • 1
GhostCat
  • 137,827
  • 25
  • 176
  • 248

3 Answers3

1

Gson is built in consideration of "I am going to be used to serialize" and "I am going to be used to deserialize".

There is no way to determine from raw JSON what the exact runtime type of a descendant of BaseBean is.

You can use RuntimeTypeAdapterFactory as described here - unfortunately it's not published with the base Gson module nor is it in Maven Central as described here. This will publish enough information with the JSON that'll allow Gson to deserialize it.

Not a JD
  • 1,864
  • 6
  • 14
  • Good point, but unfortunately, in our code base, the Gson instance comes in on some "generic" path, and I cant add custom type adapters for such specific cases. – GhostCat Mar 18 '19 at 13:53
  • I don't think it'll be possible to do then. Gson has no way of knowing on the deserialization path what the type in question is, so it'll default to the safest possible route :( At runtime, can you be 100% sure the only type you're serializing is ChildBean (and not other descendants such as AnotherChildBeanType, OneMoreChildBeanType etc)? If so, there's a hacky workaround I've deployed before. – Not a JD Mar 18 '19 at 13:58
1

Generally collection implementations "takes" type from collection field declaration - not from given item on the List/Set/etc. We need to write custom serialiser which for each item find serialiser and use it. Simple implementation:

class TypeAwareListJsonSeserializer implements JsonSerializer<List<?>> {
    @Override
    public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
        if (src == null) {
            return JsonNull.INSTANCE;
        }
        JsonArray array = new JsonArray();
        for (Object item : src) {
            JsonElement jsonElement = context.serialize(item, item.getClass());
            array.add(jsonElement);
        }
        return array;
    }
}

And this is how we can use it:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.JsonAdapter;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

public class GsonApp {

    public static void main(String[] args) throws Exception {
        List<BaseBean> children = Arrays.asList(new BaseBean(), new ChildBean(), new ChildBean2());
        BaseBeanHolder baseHolder = new BaseBeanHolder(children);
        Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .create();
        System.out.println(gson.toJson(baseHolder));
    }
}

class BaseBean {
    String baseField = "base";
}

class ChildBean extends BaseBean {
    String childField = "child";
}

class ChildBean2 extends BaseBean {
    int bean2Int = 356;
}

class BaseBeanHolder {

    @JsonAdapter(TypeAwareListJsonSeserializer.class)
    private List<? extends BaseBean> beans;

    // getters, setters, toString
}

Above code prints:

{
  "beans": [
    {
      "baseField": "base"
    },
    {
      "childField": "child",
      "baseField": "base"
    },
    {
      "bean2Int": 356,
      "baseField": "base"
    }
  ]
}

EDIT

During serialisation we lose information about type which will be needed during deserialisation process. I developed simple type information which will be stored during serialisation and used in deserialisation. It could look like below:

class TypeAwareListJsonAdapter implements JsonSerializer<List<?>>, JsonDeserializer<List<?>> {

    private final String typeProperty = "@type";

    @Override
    public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
        if (src == null) {
            return JsonNull.INSTANCE;
        }
        JsonArray array = new JsonArray();
        for (Object item : src) {
            JsonObject jsonElement = (JsonObject) context.serialize(item, item.getClass());
            jsonElement.addProperty(typeProperty, item.getClass().getSimpleName());

            array.add(jsonElement);
        }
        return array;
    }

    @Override
    public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        final Type elementType = $Gson$Types.getCollectionElementType(typeOfT, List.class);

        if (json instanceof JsonArray) {
            final JsonArray array = (JsonArray) json;
            final int size = array.size();
            if (size == 0) {
                return Collections.emptyList();
            }

            final List<?> suites = new ArrayList<>(size);
            for (int i = 0; i < size; i++) {
                JsonObject jsonElement = (JsonObject) array.get(i);
                String simpleName = jsonElement.get(typeProperty).getAsString();
                suites.add(context.deserialize(jsonElement, getClass(simpleName, elementType)));
            }

            return suites;
        }

        return Collections.emptyList();
    }

    private Type getClass(String simpleName, Type defaultType) {
        try {
            // you can use mapping or something else...
            return Class.forName("com.model." + simpleName);
        } catch (ClassNotFoundException e) {
            return defaultType;
        }
    }
}

The biggest problem is to how to map classes to JSON values. We can use class simple name or provide Map<String, Class> and use it. Now, we can use it as above. Example app prints now:

{
  "beans": [
    {
      "baseField": "base",
      "@type": "BaseBean"
    },
    {
      "childField": "child",
      "baseField": "base",
      "@type": "ChildBean"
    },
    {
      "bean2Int": 356,
      "baseField": "base",
      "@type": "ChildBean2"
    }
  ]
}
BaseBean{baseField='base'}
ChildBean{baseField='base', childField='child'}
ChildBean2{baseField='base', bean2Int=356}
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • Very nice answer. Now I am wondering: is there a similar elegant way to get de-serialization in a somehow "symmetric" way? Or would that require a distinct type adapter on the gson instance? – GhostCat Mar 18 '19 at 18:42
  • @GhostCat, I think my [answer](https://stackoverflow.com/questions/55098886/mapping-json-array-to-java-models#55120356) for [Mapping Json Array to Java Models](https://stackoverflow.com/questions/55098886/mapping-json-array-to-java-models) question could be useful. It needs some refactoring but mostly it contains everything to deserialise given `JSON` object to type. I will try to "reverse" implementation and update answer. – Michał Ziober Mar 18 '19 at 18:49
  • @GhostCat, during serialisation we lost type related information. We can in deserialiser try to deserialise to many subtypes and check `fit rate` for given object but it will be problematic with many subtypes. The easiest way is to include extra field during serialisation - like class simple name and use this info during deserialisation. Of course, if we want to have one type adapter class for serialisation and deserialisation it should extend two interfaces: `JsonSerializer>` and `JsonDeserializer>`. – Michał Ziober Mar 18 '19 at 19:26
  • 1
    Yeah, I had similar ideas. But all fine right now. My biggest problem right now is to decide whether I want to use your solution, or my workaround (see below, using arrays) or to step back and simply use the two different holder classes (which means more code, but more expressive types to be used for the different use cases I need). – GhostCat Mar 18 '19 at 19:31
  • 1
    @GhostCat, trick with array is great! I was not aware that array serialisation process is different than for `collections`. I added simple `type-aware` serialisation/deserialisation example. Hope it will help you somehow. – Michał Ziober Mar 18 '19 at 19:48
1

More of an addendum: I just figured that at least serialization works fine with arrays, so a simple workaround was to rework the holder:

static class BaseBeanHolder {
    BaseBean[] beans;
    public BaseBeanHolder(BaseBean... beans) { this.beans = beans; }
}
GhostCat
  • 137,827
  • 25
  • 176
  • 248