Fix the map type adapter to support array serialization natively.

This commit is contained in:
Jesse Wilson 2011-09-12 05:51:17 +00:00
parent d43cf5ea35
commit a98d6eae47
8 changed files with 183 additions and 149 deletions

View File

@ -170,8 +170,8 @@ public final class Gson {
this(DEFAULT_EXCLUSION_STRATEGY, DEFAULT_EXCLUSION_STRATEGY, DEFAULT_NAMING_POLICY, this(DEFAULT_EXCLUSION_STRATEGY, DEFAULT_EXCLUSION_STRATEGY, DEFAULT_NAMING_POLICY,
DefaultTypeAdapters.getDefaultInstanceCreators(), DefaultTypeAdapters.getDefaultInstanceCreators(),
false, DefaultTypeAdapters.getAllDefaultSerializers(), false, DefaultTypeAdapters.getAllDefaultSerializers(),
DefaultTypeAdapters.getAllDefaultDeserializers(), DEFAULT_JSON_NON_EXECUTABLE, true, false, DefaultTypeAdapters.getAllDefaultDeserializers(), false, DEFAULT_JSON_NON_EXECUTABLE, true,
false, LongSerializationPolicy.DEFAULT); false, false, LongSerializationPolicy.DEFAULT);
} }
Gson(final ExclusionStrategy deserializationExclusionStrategy, Gson(final ExclusionStrategy deserializationExclusionStrategy,
@ -180,8 +180,9 @@ public final class Gson {
final ParameterizedTypeHandlerMap<InstanceCreator<?>> instanceCreators, boolean serializeNulls, final ParameterizedTypeHandlerMap<InstanceCreator<?>> instanceCreators, boolean serializeNulls,
final ParameterizedTypeHandlerMap<JsonSerializer<?>> serializers, final ParameterizedTypeHandlerMap<JsonSerializer<?>> serializers,
final ParameterizedTypeHandlerMap<JsonDeserializer<?>> deserializers, final ParameterizedTypeHandlerMap<JsonDeserializer<?>> deserializers,
boolean generateNonExecutableGson, boolean htmlSafe, boolean prettyPrinting, boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe,
boolean serializeSpecialFloatingPointValues, LongSerializationPolicy longSerializationPolicy) { boolean prettyPrinting, boolean serializeSpecialFloatingPointValues,
LongSerializationPolicy longSerializationPolicy) {
this.deserializationExclusionStrategy = deserializationExclusionStrategy; this.deserializationExclusionStrategy = deserializationExclusionStrategy;
this.serializationExclusionStrategy = serializationExclusionStrategy; this.serializationExclusionStrategy = serializationExclusionStrategy;
this.fieldNamingPolicy = fieldNamingPolicy; this.fieldNamingPolicy = fieldNamingPolicy;
@ -245,12 +246,12 @@ public final class Gson {
.factory(TypeAdapters.INET_ADDRESS_FACTORY) .factory(TypeAdapters.INET_ADDRESS_FACTORY)
.typeAdapter(BigDecimal.class, new BigDecimalTypeAdapter()) .typeAdapter(BigDecimal.class, new BigDecimalTypeAdapter())
.typeAdapter(BigInteger.class, new BigIntegerTypeAdapter()) .typeAdapter(BigInteger.class, new BigIntegerTypeAdapter())
.factory(new MapTypeAdapterFactory(constructorConstructor))
.factory(new CollectionTypeAdapterFactory(constructorConstructor)) .factory(new CollectionTypeAdapterFactory(constructorConstructor))
.factory(ObjectTypeAdapter.FACTORY) .factory(ObjectTypeAdapter.FACTORY)
.factory(new GsonToMiniGsonTypeAdapterFactory(serializers, deserializers, .factory(new GsonToMiniGsonTypeAdapterFactory(serializers, deserializers,
new JsonDeserializationContext(this), new JsonSerializationContext(this), serializeNulls new JsonDeserializationContext(this), new JsonSerializationContext(this), serializeNulls
)) ))
.factory(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization))
.factory(ArrayTypeAdapter.FACTORY) .factory(ArrayTypeAdapter.FACTORY)
.factory(reflectiveTypeAdapterFactory); .factory(reflectiveTypeAdapterFactory);

View File

@ -28,7 +28,6 @@ import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -65,8 +64,6 @@ import java.util.Set;
* @author Joel Leitch * @author Joel Leitch
*/ */
public final class GsonBuilder { public final class GsonBuilder {
private static final MapAsArrayTypeAdapter COMPLEX_KEY_MAP_TYPE_ADAPTER =
new MapAsArrayTypeAdapter();
private static final InnerClassExclusionStrategy innerClassExclusionStrategy = private static final InnerClassExclusionStrategy innerClassExclusionStrategy =
new InnerClassExclusionStrategy(); new InnerClassExclusionStrategy();
private static final ExposeAnnotationDeserializationExclusionStrategy private static final ExposeAnnotationDeserializationExclusionStrategy
@ -94,6 +91,7 @@ public final class GsonBuilder {
private String datePattern; private String datePattern;
private int dateStyle; private int dateStyle;
private int timeStyle; private int timeStyle;
private boolean complexMapKeySerialization = false;
private boolean serializeSpecialFloatingPointValues; private boolean serializeSpecialFloatingPointValues;
private boolean escapeHtmlChars; private boolean escapeHtmlChars;
private boolean prettyPrinting; private boolean prettyPrinting;
@ -273,7 +271,7 @@ public final class GsonBuilder {
* @since 1.7 * @since 1.7
*/ */
public GsonBuilder enableComplexMapKeySerialization() { public GsonBuilder enableComplexMapKeySerialization() {
registerTypeHierarchyAdapter(Map.class, COMPLEX_KEY_MAP_TYPE_ADAPTER); complexMapKeySerialization = true;
return this; return this;
} }
@ -694,8 +692,9 @@ public final class GsonBuilder {
return new Gson(new DisjunctionExclusionStrategy(deserializationStrategies), return new Gson(new DisjunctionExclusionStrategy(deserializationStrategies),
new DisjunctionExclusionStrategy(serializationStrategies), new DisjunctionExclusionStrategy(serializationStrategies),
fieldNamingPolicy, instanceCreators, serializeNulls, fieldNamingPolicy, instanceCreators, serializeNulls,
customSerializers, customDeserializers, generateNonExecutableJson, escapeHtmlChars, customSerializers, customDeserializers, complexMapKeySerialization,
prettyPrinting, serializeSpecialFloatingPointValues, longSerializationPolicy); generateNonExecutableJson, escapeHtmlChars, prettyPrinting,
serializeSpecialFloatingPointValues, longSerializationPolicy);
} }
private static void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle, private static void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle,

View File

@ -1,102 +0,0 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.gson;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Adapts maps containing complex keys as arrays of map entries.
*
* <h3>Maps as JSON objects</h3>
* The standard GSON map type adapter converts Java {@link Map Maps} to JSON
* Objects. This requires that map keys can be serialized as strings; this is
* insufficient for some key types. For example, consider a map whose keys are
* points on a grid. The default JSON form encodes reasonably: <pre> {@code
* Map<Point, String> original = new LinkedHashMap<Point, String>();
* original.put(new Point(5, 6), "a");
* original.put(new Point(8, 8), "b");
* System.out.println(gson.toJson(original, type));
* }</pre>
* The above code prints this JSON object:<pre> {@code
* {
* "(5,6)": "a",
* "(8,8)": "b"
* }
* }</pre>
* But GSON is unable to deserialize this value because the JSON string name is
* just the {@link Object#toString() toString()} of the map key. Attempting to
* convert the above JSON to an object fails with a parse exception:
* <pre>com.google.gson.JsonParseException: Expecting object found: "(5,6)"
* at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler
* at com.google.gson.ObjectNavigator.navigateClassFields
* ...</pre>
*
* <h3>Maps as JSON arrays</h3>
* An alternative approach taken by this type adapter is to encode maps as
* arrays of map entries. Each map entry is a two element array containing a key
* and a value. This approach is more flexible because any type can be used as
* the map's key; not just strings. But it's also less portable because the
* receiver of such JSON must be aware of the map entry convention.
*
* <p>Register this adapter when you are creating your GSON instance.
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
* .create();
* }</pre>
* This will change the structure of the JSON emitted by the code above. Now we
* get an array. In this case the arrays elements are map entries:
* <pre> {@code
* [
* [
* {
* "x": 5,
* "y": 6
* },
* "a",
* ],
* [
* {
* "x": 8,
* "y": 8
* },
* "b"
* ]
* ]
* }</pre>
* This format will serialize and deserialize just fine as long as this adapter
* is registered.
*
* <p>This adapter returns regular JSON objects for maps whose keys are not
* complex. A key is complex if its JSON-serialized form is an array or an
* object.
*/
final class MapAsArrayTypeAdapter
implements JsonSerializer<Map<?, ?>>, JsonDeserializer<Map<?, ?>> {
public Map<?, ?> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
// TODO
throw new UnsupportedOperationException();
}
public JsonElement serialize(Map<?, ?> src, Type typeOfSrc, JsonSerializationContext context) {
// TODO
throw new UnsupportedOperationException();
}
}

View File

@ -323,9 +323,13 @@ public final class $Gson$Types {
} }
Type mapType = getSupertype(context, contextRawType, Map.class); Type mapType = getSupertype(context, contextRawType, Map.class);
// TODO: strip wildcards?
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType; ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments(); return mapParameterizedType.getActualTypeArguments();
} }
return new Type[] { Object.class, Object.class };
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) { public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {

View File

@ -16,25 +16,96 @@
package com.google.gson.internal.bind; package com.google.gson.internal.bind;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.internal.$Gson$Types; import com.google.gson.internal.$Gson$Types;
import com.google.gson.internal.ConstructorConstructor; import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.ObjectConstructor; import com.google.gson.internal.ObjectConstructor;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Adapt a map whose keys are any type. * Adapts maps to either JSON objects or JSON arrays.
*
* <h3>Maps as JSON objects</h3>
* For primitive keys or when complex map key serialization is not enabled, this
* converts Java {@link Map Maps} to JSON Objects. This requires that map keys
* can be serialized as strings; this is insufficient for some key types. For
* example, consider a map whose keys are points on a grid. The default JSON
* form encodes reasonably: <pre> {@code
* Map<Point, String> original = new LinkedHashMap<Point, String>();
* original.put(new Point(5, 6), "a");
* original.put(new Point(8, 8), "b");
* System.out.println(gson.toJson(original, type));
* }</pre>
* The above code prints this JSON object:<pre> {@code
* {
* "(5,6)": "a",
* "(8,8)": "b"
* }
* }</pre>
* But GSON is unable to deserialize this value because the JSON string name is
* just the {@link Object#toString() toString()} of the map key. Attempting to
* convert the above JSON to an object fails with a parse exception:
* <pre>com.google.gson.JsonParseException: Expecting object found: "(5,6)"
* at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler
* at com.google.gson.ObjectNavigator.navigateClassFields
* ...</pre>
*
* <h3>Maps as JSON arrays</h3>
* An alternative approach taken by this type adapter when it is required and
* complex map key serialization is enabled is to encode maps as arrays of map
* entries. Each map entry is a two element array containing a key and a value.
* This approach is more flexible because any type can be used as the map's key;
* not just strings. But it's also less portable because the receiver of such
* JSON must be aware of the map entry convention.
*
* <p>Register this adapter when you are creating your GSON instance.
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
* .create();
* }</pre>
* This will change the structure of the JSON emitted by the code above. Now we
* get an array. In this case the arrays elements are map entries:
* <pre> {@code
* [
* [
* {
* "x": 5,
* "y": 6
* },
* "a",
* ],
* [
* {
* "x": 8,
* "y": 8
* },
* "b"
* ]
* ]
* }</pre>
* This format will serialize and deserialize just fine as long as this adapter
* is registered.
*/ */
public final class MapTypeAdapterFactory implements TypeAdapter.Factory { public final class MapTypeAdapterFactory implements TypeAdapter.Factory {
private final ConstructorConstructor constructorConstructor; private final ConstructorConstructor constructorConstructor;
private final boolean complexMapKeySerialization;
public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor) { public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor,
boolean complexMapKeySerialization) {
this.constructorConstructor = constructorConstructor; this.constructorConstructor = constructorConstructor;
this.complexMapKeySerialization = complexMapKeySerialization;
} }
public <T> TypeAdapter<T> create(MiniGson context, TypeToken<T> typeToken) { public <T> TypeAdapter<T> create(MiniGson context, TypeToken<T> typeToken) {
@ -46,56 +117,120 @@ public final class MapTypeAdapterFactory implements TypeAdapter.Factory {
} }
Class<?> rawTypeOfSrc = $Gson$Types.getRawType(type); Class<?> rawTypeOfSrc = $Gson$Types.getRawType(type);
Type childGenericType = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc)[1]; Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc);
TypeAdapter valueAdapter = context.getAdapter(TypeToken.get(childGenericType)); TypeAdapter<?> keyAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[0]));
TypeAdapter<?> valueAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[1]));
ObjectConstructor<T> constructor = constructorConstructor.getConstructor(typeToken); ObjectConstructor<T> constructor = constructorConstructor.getConstructor(typeToken);
@SuppressWarnings("unchecked") // we don't define a type parameter for the key or value types @SuppressWarnings("unchecked") // we don't define a type parameter for the key or value types
TypeAdapter<T> result = new Adapter(valueAdapter, constructor); TypeAdapter<T> result = new Adapter(keyAdapter, valueAdapter, constructor);
return result; return result;
} }
private final class Adapter<V> extends TypeAdapter<Map<?, V>> { private final class Adapter<K, V> extends TypeAdapter<Map<K, V>> {
private final TypeAdapter<K> keyTypeAdapter;
private final TypeAdapter<V> valueTypeAdapter; private final TypeAdapter<V> valueTypeAdapter;
private final ObjectConstructor<? extends Map<String, V>> constructor; private final ObjectConstructor<? extends Map<K, V>> constructor;
public Adapter(TypeAdapter<V> valueTypeAdapter, public Adapter(TypeAdapter<K> keyTypeAdapter, TypeAdapter<V> valueTypeAdapter,
ObjectConstructor<? extends Map<String, V>> constructor) { ObjectConstructor<? extends Map<K, V>> constructor) {
this.keyTypeAdapter = keyTypeAdapter;
this.valueTypeAdapter = valueTypeAdapter; this.valueTypeAdapter = valueTypeAdapter;
this.constructor = constructor; this.constructor = constructor;
} }
public Map<?, V> read(JsonReader reader) throws IOException { public Map<K, V> read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) { JsonToken peek = reader.peek();
if (peek == JsonToken.NULL) {
reader.nextNull(); // TODO: does this belong here? reader.nextNull(); // TODO: does this belong here?
return null; return null;
} }
Map<String, V> map = constructor.construct(); Map<K, V> map = constructor.construct();
if (peek == JsonToken.BEGIN_ARRAY) {
reader.beginArray();
while (reader.hasNext()) {
reader.beginArray(); // entry array
K key = keyTypeAdapter.read(reader);
V value = valueTypeAdapter.read(reader);
V replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
reader.endArray();
}
reader.endArray();
} else {
reader.beginObject(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
String key = reader.nextName(); String keyString = reader.nextName();
K key = keyTypeAdapter.fromJsonElement(new JsonPrimitive(keyString));
V value = valueTypeAdapter.read(reader); V value = valueTypeAdapter.read(reader);
map.put(key, value); // TODO: convert to the map's key type? V replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
} }
reader.endObject(); reader.endObject();
}
return map; return map;
} }
public void write(JsonWriter writer, Map<?, V> map) throws IOException { public void write(JsonWriter writer, Map<K, V> map) throws IOException {
if (map == null) { if (map == null) {
writer.nullValue(); // TODO: better policy here? writer.nullValue(); // TODO: better policy here?
return; return;
} }
boolean hasComplexKeys = false;
List<JsonElement> keys = new ArrayList<JsonElement>(map.size());
List<V> values = new ArrayList<V>(map.size());
for (Map.Entry<K, V> entry : map.entrySet()) {
JsonElement keyElement = keyTypeAdapter.toJsonElement(entry.getKey());
keys.add(keyElement);
values.add(entry.getValue());
hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject();
}
if (complexMapKeySerialization && hasComplexKeys) {
writer.beginArray();
for (int i = 0; i < keys.size(); i++) {
writer.beginArray(); // entry array
Streams.write(keys.get(i), true, writer);
valueTypeAdapter.write(writer, values.get(i));
writer.endArray();
}
writer.endArray();
} else {
writer.beginObject(); writer.beginObject();
for (Map.Entry<?, V> entry : map.entrySet()) { for (int i = 0; i < keys.size(); i++) {
String key = String.valueOf(entry.getKey()); JsonElement keyElement = keys.get(i);
writer.name(key); writer.name(keyToString(keyElement));
valueTypeAdapter.write(writer, entry.getValue()); valueTypeAdapter.write(writer, values.get(i));
} }
writer.endObject(); writer.endObject();
} }
} }
private String keyToString(JsonElement keyElement) {
if (keyElement.isJsonPrimitive()) {
JsonPrimitive primitive = keyElement.getAsJsonPrimitive();
if (primitive.isNumber()) {
return String.valueOf(primitive.getAsNumber());
} else if (primitive.isBoolean()) {
return Boolean.toString(primitive.getAsBoolean());
} else if (primitive.isString()) {
return primitive.getAsString();
} else {
throw new AssertionError();
}
} else if (keyElement.isJsonNull()) {
return "null";
} else {
throw new AssertionError();
}
}
}
} }

View File

@ -39,8 +39,8 @@ public class FunctionWithInternalDependenciesTest extends TestCase {
Gson gson = new Gson(exclusionStrategy, exclusionStrategy, Gson.DEFAULT_NAMING_POLICY, Gson gson = new Gson(exclusionStrategy, exclusionStrategy, Gson.DEFAULT_NAMING_POLICY,
DefaultTypeAdapters.getDefaultInstanceCreators(), DefaultTypeAdapters.getDefaultInstanceCreators(),
false, DefaultTypeAdapters.getDefaultSerializers(), false, DefaultTypeAdapters.getDefaultSerializers(),
DefaultTypeAdapters.getDefaultDeserializers(), Gson.DEFAULT_JSON_NON_EXECUTABLE, true, DefaultTypeAdapters.getDefaultDeserializers(), false, Gson.DEFAULT_JSON_NON_EXECUTABLE,
false, false, LongSerializationPolicy.DEFAULT); true, false, false, LongSerializationPolicy.DEFAULT);
assertEquals("{}", gson.toJson(new ClassWithNoFields() { assertEquals("{}", gson.toJson(new ClassWithNoFields() {
// empty anonymous class // empty anonymous class
})); }));

View File

@ -53,7 +53,7 @@ public class MapAsArrayTypeAdapterTest extends TestCase {
new TypeToken<Map<String, Boolean>>() {}.getType())); new TypeToken<Map<String, Boolean>>() {}.getType()));
} }
public void testTwoTypesCollapseToOneSerialize() { public void disabled_testTwoTypesCollapseToOneSerialize() {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.enableComplexMapKeySerialization() .enableComplexMapKeySerialization()
.create(); .create();
@ -63,7 +63,7 @@ public class MapAsArrayTypeAdapterTest extends TestCase {
original.put(new Float(1.0), "b"); original.put(new Float(1.0), "b");
try { try {
gson.toJson(original, new TypeToken<Map<Number, String>>() {}.getType()); gson.toJson(original, new TypeToken<Map<Number, String>>() {}.getType());
fail(); fail(); // we no longer hash keys at serialization time
} catch (JsonSyntaxException expected) { } catch (JsonSyntaxException expected) {
} }
} }

View File

@ -18,13 +18,10 @@ package com.google.gson.internal;
import com.google.gson.common.TestTypes.Base; import com.google.gson.common.TestTypes.Base;
import com.google.gson.common.TestTypes.Sub; import com.google.gson.common.TestTypes.Sub;
import com.google.gson.internal.ParameterizedTypeHandlerMap;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import junit.framework.TestCase;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import junit.framework.TestCase;
/** /**
* Unit tests for the {@link com.google.gson.internal.ParameterizedTypeHandlerMap} class. * Unit tests for the {@link com.google.gson.internal.ParameterizedTypeHandlerMap} class.