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

View File

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

@ -298,7 +298,7 @@ public final class $Gson$Types {
*/
public static Type getCollectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof WildcardType) {
collectionType = ((WildcardType)collectionType).getUpperBounds()[0];
}
@ -323,8 +323,12 @@ public final class $Gson$Types {
}
Type mapType = getSupertype(context, contextRawType, Map.class);
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
// TODO: strip wildcards?
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
}
return new Type[] { Object.class, Object.class };
}
@SuppressWarnings("unchecked")

View File

@ -16,25 +16,96 @@
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.ConstructorConstructor;
import com.google.gson.internal.ObjectConstructor;
import com.google.gson.internal.Streams;
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;
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 {
private final ConstructorConstructor constructorConstructor;
private final boolean complexMapKeySerialization;
public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor,
boolean complexMapKeySerialization) {
this.constructorConstructor = constructorConstructor;
this.complexMapKeySerialization = complexMapKeySerialization;
}
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);
Type childGenericType = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc)[1];
TypeAdapter valueAdapter = context.getAdapter(TypeToken.get(childGenericType));
Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc);
TypeAdapter<?> keyAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[0]));
TypeAdapter<?> valueAdapter = context.getAdapter(TypeToken.get(keyAndValueTypes[1]));
ObjectConstructor<T> constructor = constructorConstructor.getConstructor(typeToken);
@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;
}
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 ObjectConstructor<? extends Map<String, V>> constructor;
private final ObjectConstructor<? extends Map<K, V>> constructor;
public Adapter(TypeAdapter<V> valueTypeAdapter,
ObjectConstructor<? extends Map<String, V>> constructor) {
public Adapter(TypeAdapter<K> keyTypeAdapter, TypeAdapter<V> valueTypeAdapter,
ObjectConstructor<? extends Map<K, V>> constructor) {
this.keyTypeAdapter = keyTypeAdapter;
this.valueTypeAdapter = valueTypeAdapter;
this.constructor = constructor;
}
public Map<?, V> read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
public Map<K, V> read(JsonReader reader) throws IOException {
JsonToken peek = reader.peek();
if (peek == JsonToken.NULL) {
reader.nextNull(); // TODO: does this belong here?
return null;
}
Map<String, V> map = constructor.construct();
Map<K, V> map = constructor.construct();
reader.beginObject();
while (reader.hasNext()) {
String key = reader.nextName();
V value = valueTypeAdapter.read(reader);
map.put(key, value); // TODO: convert to the map's key type?
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();
while (reader.hasNext()) {
String keyString = reader.nextName();
K key = keyTypeAdapter.fromJsonElement(new JsonPrimitive(keyString));
V value = valueTypeAdapter.read(reader);
V replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
}
reader.endObject();
}
reader.endObject();
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) {
writer.nullValue(); // TODO: better policy here?
return;
}
writer.beginObject();
for (Map.Entry<?, V> entry : map.entrySet()) {
String key = String.valueOf(entry.getKey());
writer.name(key);
valueTypeAdapter.write(writer, entry.getValue());
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();
for (int i = 0; i < keys.size(); i++) {
JsonElement keyElement = keys.get(i);
writer.name(keyToString(keyElement));
valueTypeAdapter.write(writer, values.get(i));
}
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();
}
writer.endObject();
}
}
}

View File

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

View File

@ -53,7 +53,7 @@ public class MapAsArrayTypeAdapterTest extends TestCase {
new TypeToken<Map<String, Boolean>>() {}.getType()));
}
public void testTwoTypesCollapseToOneSerialize() {
public void disabled_testTwoTypesCollapseToOneSerialize() {
Gson gson = new GsonBuilder()
.enableComplexMapKeySerialization()
.create();
@ -63,7 +63,7 @@ public class MapAsArrayTypeAdapterTest extends TestCase {
original.put(new Float(1.0), "b");
try {
gson.toJson(original, new TypeToken<Map<Number, String>>() {}.getType());
fail();
fail(); // we no longer hash keys at serialization time
} 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.Sub;
import com.google.gson.internal.ParameterizedTypeHandlerMap;
import com.google.gson.reflect.TypeToken;
import junit.framework.TestCase;
import java.lang.reflect.Type;
import java.util.List;
import junit.framework.TestCase;
/**
* Unit tests for the {@link com.google.gson.internal.ParameterizedTypeHandlerMap} class.