diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 27f3ee9246..27983bed51 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -43,6 +43,7 @@ import com.google.gson.internal.bind.ArrayTypeAdapter; import com.google.gson.internal.bind.CollectionTypeAdapterFactory; import com.google.gson.internal.bind.DateTypeAdapter; +import com.google.gson.internal.bind.IterableTypeAdapterFactory; import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory; import com.google.gson.internal.bind.JsonTreeReader; import com.google.gson.internal.bind.JsonTreeWriter; @@ -271,6 +272,7 @@ public Gson() { // type adapters for composite and user-defined types factories.add(new CollectionTypeAdapterFactory(constructorConstructor)); factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization)); + factories.add(IterableTypeAdapterFactory.INSTANCE); this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor); factories.add(jsonAdapterFactory); factories.add(TypeAdapters.ENUM_FACTORY); diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java index adea605f59..02560a12cd 100644 --- a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java +++ b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java @@ -295,17 +295,27 @@ public static Type getArrayComponentType(Type array) { } /** - * Returns the element type of this collection type. - * @throws IllegalArgumentException if this type is not a collection. + * Returns the element type of this {@link Collection} type. + * @throws IllegalArgumentException if this type is not a {@code Collection}. */ public static Type getCollectionElementType(Type context, Class contextRawType) { - Type collectionType = getSupertype(context, contextRawType, Collection.class); + checkArgument(Collection.class.isAssignableFrom(contextRawType)); + // `Collection extends Iterable`, so can delegate to that method + return getIterableElementType(context, contextRawType); + } + + /** + * Returns the element type of this {@link Iterable} type. + * @throws IllegalArgumentException if this type is not a {@code Iterable}. + */ + public static Type getIterableElementType(Type context, Class contextRawType) { + Type iterableType = getSupertype(context, contextRawType, Iterable.class); - if (collectionType instanceof WildcardType) { - collectionType = ((WildcardType)collectionType).getUpperBounds()[0]; + if (iterableType instanceof WildcardType) { + iterableType = ((WildcardType)iterableType).getUpperBounds()[0]; } - if (collectionType instanceof ParameterizedType) { - return ((ParameterizedType) collectionType).getActualTypeArguments()[0]; + if (iterableType instanceof ParameterizedType) { + return ((ParameterizedType) iterableType).getActualTypeArguments()[0]; } return Object.class; } diff --git a/gson/src/main/java/com/google/gson/internal/bind/IterableTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/IterableTypeAdapterFactory.java new file mode 100644 index 0000000000..55c003d45c --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/IterableTypeAdapterFactory.java @@ -0,0 +1,80 @@ +package com.google.gson.internal.bind; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.$Gson$Types; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public final class IterableTypeAdapterFactory implements TypeAdapterFactory { + public static final IterableTypeAdapterFactory INSTANCE = new IterableTypeAdapterFactory(); + + private IterableTypeAdapterFactory() { + } + + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + Type type = typeToken.getType(); + + Class rawType = typeToken.getRawType(); + /* + * Only support Iterable, but not subtypes + * This allows freely choosing Iterable implementation on deserialization + */ + if (rawType != Iterable.class) { + return null; + } + + Type elementType = $Gson$Types.getIterableElementType(type, rawType); + TypeAdapter elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType)); + + @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter + TypeAdapter result = new Adapter(gson, elementType, elementTypeAdapter); + return result; + } + + private static final class Adapter extends TypeAdapter> { + private final TypeAdapter elementTypeAdapter; + + public Adapter(Gson context, Type elementType, TypeAdapter elementTypeAdapter) { + this.elementTypeAdapter = + new TypeAdapterRuntimeTypeWrapper(context, elementTypeAdapter, elementType); + } + + @Override public Iterable read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + List list = new ArrayList(); + in.beginArray(); + while (in.hasNext()) { + E instance = elementTypeAdapter.read(in); + list.add(instance); + } + in.endArray(); + return list; + } + + @Override public void write(JsonWriter out, Iterable iterable) throws IOException { + if (iterable == null) { + out.nullValue(); + return; + } + + out.beginArray(); + for (E element : iterable) { + elementTypeAdapter.write(out, element); + } + out.endArray(); + } + } +} diff --git a/gson/src/test/java/com/google/gson/functional/IterableTest.java b/gson/src/test/java/com/google/gson/functional/IterableTest.java new file mode 100644 index 0000000000..efb688411d --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/IterableTest.java @@ -0,0 +1,91 @@ +package com.google.gson.functional; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import junit.framework.TestCase; + +public class IterableTest extends TestCase { + private static class CustomIterable implements Iterable { + final int base; + + public CustomIterable(int base) { + this.base = base; + } + + @Override + public Iterator iterator() { + return Arrays.asList(base + 1, base + 2).iterator(); + } + } + + public void testSerialize() { + CustomIterable iterable = new CustomIterable(0); + + Gson gson = new Gson(); + // Serializing specific Iterable subtype should use reflection-based approach + assertEquals("{\"base\":0}", gson.toJson(iterable)); + // But serializing as Iterable should use adapter + assertEquals("[1,2]", gson.toJson(iterable, Iterable.class)); + } + + public void testDeserialize() { + Gson gson = new Gson(); + // Deserializing as specific Iterable subtype should use reflection-based approach + // i.e. must not choose any (potentially incompatible) class implementing `Iterable` + // See also https://github.com/google/gson/issues/1708 + assertEquals(1, gson.fromJson("{\"base\":1}", CustomIterable.class).base); + + // But deserializing as Iterable should use adapter + Iterable deserialized = gson.fromJson("[1,2]", new TypeToken>() {}.getType()); + // Collect elements and then compare them to not make any assumptions about + // type of `deserialized` + ArrayList elements = new ArrayList(); + for (Integer element : deserialized) { + elements.add(element); + } + assertEquals(Arrays.asList(1, 2), elements); + } + + private static class CustomIterableTypeAdapter extends TypeAdapter> { + private static final int SERIALIZED = 5; + + private static List getDeserialized() { + return Arrays.asList(1, 2); + } + + @Override public void write(JsonWriter out, Iterable value) throws IOException { + out.value(SERIALIZED); + } + + @Override public Iterable read(JsonReader in) throws IOException { + in.skipValue(); + return getDeserialized(); + } + } + + /** + * Verify that overwriting built-in {@link Iterable} adapter is possible. + */ + public void testCustomAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Iterable.class, new CustomIterableTypeAdapter()) + .create(); + + String expectedJson = String.valueOf(CustomIterableTypeAdapter.SERIALIZED); + assertEquals(expectedJson, gson.toJson(new ArrayList(), Iterable.class)); + + List expectedDeserialized = CustomIterableTypeAdapter.getDeserialized(); + assertEquals(expectedDeserialized, gson.fromJson("[]", Iterable.class)); + } +}