Skip to content

Commit

Permalink
Add (de-)serialization support for Iterable
Browse files Browse the repository at this point in the history
This adds only support for (de-)serializing Iterable, but not any of its
subtypes since it would not be possible to properly deserialize them again.
  • Loading branch information
Marcono1234 committed May 25, 2020
1 parent ceae88b commit c9059b2
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 7 deletions.
2 changes: 2 additions & 0 deletions gson/src/main/java/com/google/gson/Gson.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 17 additions & 7 deletions gson/src/main/java/com/google/gson/internal/$Gson$Types.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<E> extends Iterable<E>`, 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Type type = typeToken.getType();

Class<? super T> 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<T> result = new Adapter(gson, elementType, elementTypeAdapter);
return result;
}

private static final class Adapter<E> extends TypeAdapter<Iterable<E>> {
private final TypeAdapter<E> elementTypeAdapter;

public Adapter(Gson context, Type elementType, TypeAdapter<E> elementTypeAdapter) {
this.elementTypeAdapter =
new TypeAdapterRuntimeTypeWrapper<E>(context, elementTypeAdapter, elementType);
}

@Override public Iterable<E> read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}

List<E> list = new ArrayList<E>();
in.beginArray();
while (in.hasNext()) {
E instance = elementTypeAdapter.read(in);
list.add(instance);
}
in.endArray();
return list;
}

@Override public void write(JsonWriter out, Iterable<E> iterable) throws IOException {
if (iterable == null) {
out.nullValue();
return;
}

out.beginArray();
for (E element : iterable) {
elementTypeAdapter.write(out, element);
}
out.endArray();
}
}
}
91 changes: 91 additions & 0 deletions gson/src/test/java/com/google/gson/functional/IterableTest.java
Original file line number Diff line number Diff line change
@@ -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<Integer> {
final int base;

public CustomIterable(int base) {
this.base = base;
}

@Override
public Iterator<Integer> 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<Integer> deserialized = gson.fromJson("[1,2]", new TypeToken<Iterable<Integer>>() {}.getType());
// Collect elements and then compare them to not make any assumptions about
// type of `deserialized`
ArrayList<Integer> elements = new ArrayList<>();
for (Integer element : deserialized) {
elements.add(element);
}
assertEquals(Arrays.asList(1, 2), elements);
}

private static class CustomIterableTypeAdapter extends TypeAdapter<Iterable<?>> {
private static final int SERIALIZED = 5;

private static List<Integer> 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<Integer> expectedDeserialized = CustomIterableTypeAdapter.getDeserialized();
assertEquals(expectedDeserialized, gson.fromJson("[]", Iterable.class));
}
}

0 comments on commit c9059b2

Please sign in to comment.