Type adapter for maps. Supports both complex and non-complex keys.
This commit is contained in:
parent
bd3e680f94
commit
37dc0f8d3e
168
gson/src/main/java/com/google/gson/MapAsArrayTypeAdapter.java
Normal file
168
gson/src/main/java/com/google/gson/MapAsArrayTypeAdapter.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user