Type adapter for maps. Supports both complex and non-complex keys.

This commit is contained in:
Jesse Wilson 2011-01-12 00:17:21 +00:00
parent bd3e680f94
commit 37dc0f8d3e
2 changed files with 270 additions and 0 deletions

View File

@ -0,0 +1,168 @@
/*
* 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.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
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.
*/
public final class MapAsArrayTypeAdapter
implements JsonSerializer<Map<?, ?>>, JsonDeserializer<Map<?, ?>> {
public Map<?, ?> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
Map<Object, Object> result = new LinkedHashMap<Object, Object>();
Type[] keyAndValueType = typeToTypeArguments(typeOfT);
if (json.isJsonArray()) {
JsonArray array = json.getAsJsonArray();
for (int i = 0; i < array.size(); i++) {
JsonArray entryArray = array.get(i).getAsJsonArray();
Object k = context.deserialize(entryArray.get(0), keyAndValueType[0]);
Object v = context.deserialize(entryArray.get(1), keyAndValueType[1]);
result.put(k, v);
}
checkSize(array, array.size(), result, result.size());
} else {
JsonObject object = json.getAsJsonObject();
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
Object k = context.deserialize(new JsonPrimitive(entry.getKey()), keyAndValueType[0]);
Object v = context.deserialize(entry.getValue(), keyAndValueType[1]);
result.put(k, v);
}
checkSize(object, object.entrySet().size(), result, result.size());
}
return result;
}
public JsonElement serialize(Map<?, ?> src, Type typeOfSrc, JsonSerializationContext context) {
Type[] keyAndValueType = typeToTypeArguments(typeOfSrc);
boolean serializeAsArray = false;
List<JsonElement> keysAndValues = new ArrayList<JsonElement>();
for (Map.Entry<?, ?> entry : src.entrySet()) {
JsonElement key = context.serialize(entry.getKey(), keyAndValueType[0]);
serializeAsArray |= key.isJsonObject() || key.isJsonArray();
keysAndValues.add(key);
keysAndValues.add(context.serialize(entry.getValue(), keyAndValueType[1]));
}
if (serializeAsArray) {
JsonArray result = new JsonArray();
for (int i = 0; i < keysAndValues.size(); i+=2) {
JsonArray entryArray = new JsonArray();
entryArray.add(keysAndValues.get(i));
entryArray.add(keysAndValues.get(i + 1));
result.add(entryArray);
}
return result;
} else {
JsonObject result = new JsonObject();
for (int i = 0; i < keysAndValues.size(); i+=2) {
result.add(keysAndValues.get(i).getAsString(), keysAndValues.get(i + 1));
}
checkSize(src, src.size(), result, result.entrySet().size());
return result;
}
}
private Type[] typeToTypeArguments(Type typeOfT) {
if (typeOfT instanceof ParameterizedType) {
Type[] typeArguments = ((ParameterizedType) typeOfT).getActualTypeArguments();
if (typeArguments.length != 2) {
throw new IllegalArgumentException("MapAsArrayTypeAdapter cannot handle " + typeOfT);
}
return typeArguments;
}
return new Type[] { Object.class, Object.class };
}
private void checkSize(Object input, int inputSize, Object output, int outputSize) {
if (inputSize != outputSize) {
throw new JsonSyntaxException("Input size " + inputSize + " != output size " + outputSize
+ " for input " + input + " and output " + output);
}
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.functional;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.MapAsArrayTypeAdapter;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import junit.framework.TestCase;
public class MapAsArrayTypeAdapterTest extends TestCase {
public void testSerializeComplexMapWithTypeAdapter() {
Type type = new TypeToken<Map<Point, String>>() {}.getType();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
.create();
Map<Point, String> original = new LinkedHashMap<Point, String>();
original.put(new Point(5, 5), "a");
original.put(new Point(8, 8), "b");
String json = gson.toJson(original, type);
assertEquals("[[{\"x\":5,\"y\":5},\"a\"],[{\"x\":8,\"y\":8},\"b\"]]", json);
assertEquals(original, gson.<Map<Point, String>>fromJson(json, type));
// test that registering a type adapter for one map doesn't interfere with others
Map<String, Boolean> otherMap = new LinkedHashMap<String, Boolean>();
otherMap.put("t", true);
otherMap.put("f", false);
assertEquals("{\"t\":true,\"f\":false}",
gson.toJson(otherMap, Map.class));
assertEquals("{\"t\":true,\"f\":false}",
gson.toJson(otherMap, new TypeToken<Map<String, Boolean>>() {}.getType()));
assertEquals(otherMap, gson.<Object>fromJson("{\"t\":true,\"f\":false}",
new TypeToken<Map<String, Boolean>>() {}.getType()));
}
public void testTwoTypesCollapseToOneSerialize() {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
.create();
Map<Number, String> original = new LinkedHashMap<Number, String>();
original.put(new Double(1.0), "a");
original.put(new Float(1.0), "b");
try {
gson.toJson(original, new TypeToken<Map<Number, String>>() {}.getType());
fail();
} catch (JsonSyntaxException expected) {
}
}
public void testTwoTypesCollapseToOneDeserialize() {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
.create();
String s = "[[\"1.00\",\"a\"],[\"1.0\",\"b\"]]";
try {
gson.fromJson(s, new TypeToken<Map<Double, String>>() {}.getType());
fail();
} catch (JsonSyntaxException expected) {
}
}
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
Point() {}
@Override public boolean equals(Object o) {
return o instanceof Point && ((Point) o).x == x && ((Point) o).y == y;
}
@Override public int hashCode() {
return x * 37 + y;
}
@Override public String toString() {
return "(" + x + "," + y + ")";
}
}
}