简体   繁体   中英

Can't properly serialize POJO with Iterable fields using Gson

Here is my POJO I am trying to serialize:

public class Bar {
    private final Foo foo;

    private final Iterable<String> list;

    private final Iterable<Map<String, String>> listOfMaps;
}

Here is how I'm calling it

Bar bar = new Bar();
Foo foo = new Foo();
foo.field1 = "val1";
foo.field2 = "val2";
bar.foo = foo;
bar.list = ImmutableList.<String>of("fooList");
bar.listOfMaps = ImmutableList.<Map<String,String>>of(
                    ImmutableMap.<String,String>of("key", "val")
                );
new Gson().toJson(bar);

Here is the result

{"foo":{"field1":"val1","field2":"val2"},"list":{},"listOfMaps":{}}

As you can see, the POJO serializes fine, but the iterable (instance of guava collections) doesn't serialize to JSON properly. When I serialize the fields on their own, they show up fine, but it won't properly serialize when they are fields of Bar

Example:

new Gson().toJson(bar.list);

["fooList"]

It looks like an old issue for Gson and this is how it was initially designed. I have some assumptions why it might be designed like that, but I think my assumptions are weak ( Iterable is just too base type to get a particular implementation by default?; it may intersect with Collection and List ?; what if Iterable returns an infinite iterator?). You can track down the Iterable support issue through this pull request: https://github.com/google/gson/pull/854 .

First of all, why Gson behaves differently for both cases you mentioned ( .toJson(bar) and .toJson(bar.list) ). For the first case, Gson scans your bar object and it retrieves type information directly from the fields using reflection, and this is where it gets Iterable . For the second case, Gson does not have declaration type information therefore it just takes the actual class (not type!) using .getClass() losing type parameterization (strings fit this test cast just perfect), Collection in this case that has Gson support out of box. Note that the latter can also be reproduced with gson.toJson(..., new TypeToken<Iterable<String>>() {}.getType(), System.out) suggesting the given object type (not class!) explicitly.

However, you can add Iterable support yourself, but I'm not sure if the implementation below requires more work:

final class IterableMyTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory iterableTypeAdapterFactory = new IterableMyTypeAdapterFactory();

    private IterableMyTypeAdapterFactory() {
    }

    // It's an effective singleton but we do not reveal it
    static TypeAdapterFactory getIterableMyTypeAdapterFactory() {
        return iterableTypeAdapterFactory;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        // Must be an iterable, subclasses should be picked up by built-in type adapters
        if ( !Iterable.class.isAssignableFrom(typeToken.getRawType()) || Collection.class.isAssignableFrom(typeToken.getRawType()) ) {
            // Tell Gson to pick up on its own
            return null;
        }
        // Extract the element type. If it's raw, we assume java.lang.Object is in the play
        final Type elementType = getTypeParameter0(typeToken.getType());
        // Get the element type adapter
        final TypeAdapter<?> elementTypeAdapter = gson.getDelegateAdapter(this, TypeToken.get(elementType));
        // And get rid of wildcards
        @SuppressWarnings("unchecked")
        final TypeAdapter<Object> castElementTypeAdapter = (TypeAdapter<Object>) elementTypeAdapter;
        // Instantiating a new iterable type adapter
        final TypeAdapter<Iterable<Object>> iterableTypeAdapter = IterableTypeAdapter.get(castElementTypeAdapter);
        // Cast it cheating javac
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) iterableTypeAdapter;
        return castTypeAdapter;
    }

    private static Type getTypeParameter0(final Type type) {
        if ( !(type instanceof ParameterizedType) ) {
            return Object.class;
        }
        final ParameterizedType parameterizedType = (ParameterizedType) type;
        return parameterizedType.getActualTypeArguments()[0];
    }

    private static final class IterableTypeAdapter<E>
            extends TypeAdapter<Iterable<E>> {

        private final TypeAdapter<E> elementTypeAdapter;

        private IterableTypeAdapter(final TypeAdapter<E> elementTypeAdapter) {
            this.elementTypeAdapter = elementTypeAdapter;
        }

        private static <E> TypeAdapter<Iterable<E>> get(final TypeAdapter<E> elementTypeAdapter) {
            return new IterableTypeAdapter<>(elementTypeAdapter)
                    .nullSafe(); // We don't need to handle nulls ourselves anymore
        }

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter jsonWriter, final Iterable<E> elements)
                throws IOException {
            // Emit [
            jsonWriter.beginArray();
            for ( final E e : elements ) {
                // Write each element to the downstream writer
                elementTypeAdapter.write(jsonWriter, e);
            }
            // Emit ]
            jsonWriter.endArray();
        }

        @Override
        @SuppressWarnings("resource")
        public Iterable<E> read(final JsonReader jsonReader)
                throws IOException {
            jsonReader.beginArray(); // Expect [
            final Collection<E> elements = new ArrayList<>(); // This is probably why there is Iterable support by default
            while ( jsonReader.hasNext() ) {
                final E e = elementTypeAdapter.read(jsonReader); // Read each element
                elements.add(e);
            }
            jsonReader.endArray(); // Expect ]
            return elements;
        }

    }

}
private static final class Pack {

    Iterable<String> iterable;
    Collection<String> collection;
    List<String> list;
    Map<String, String> map;

}

private static final Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(getIterableMyTypeAdapterFactory())
        .create();

public static void main(final String... args) {
    final List<String> fooBar = ImmutableList.of("foo", "bar");
    final Pack pack = new Pack();
    pack.iterable = fooBar;
    pack.collection = fooBar;
    pack.list = fooBar;
    pack.map = ImmutableMap.of("foo", "bar");
    gson.toJson(pack, System.out);
}

Output before:

{"iterable":{},"collection":["foo","bar"],"list":["foo","bar"],"map":{"foo":"bar"}}

Output after:

{"iterable":["foo","bar"],"collection":["foo","bar"],"list":["foo","bar"],"map":{"foo":"bar"}}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM