2 * Copyright (C) 2011 Google Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com
.google
.gson
.internal
.bind
;
19 import com
.google
.gson
.Gson
;
20 import com
.google
.gson
.JsonElement
;
21 import com
.google
.gson
.JsonPrimitive
;
22 import com
.google
.gson
.JsonSyntaxException
;
23 import com
.google
.gson
.TypeAdapter
;
24 import com
.google
.gson
.TypeAdapterFactory
;
25 import com
.google
.gson
.internal
.$Gson$Types
;
26 import com
.google
.gson
.internal
.ConstructorConstructor
;
27 import com
.google
.gson
.internal
.JsonReaderInternalAccess
;
28 import com
.google
.gson
.internal
.ObjectConstructor
;
29 import com
.google
.gson
.internal
.Streams
;
30 import com
.google
.gson
.reflect
.TypeToken
;
31 import com
.google
.gson
.stream
.JsonReader
;
32 import com
.google
.gson
.stream
.JsonToken
;
33 import com
.google
.gson
.stream
.JsonWriter
;
34 import java
.io
.IOException
;
35 import java
.lang
.reflect
.Type
;
36 import java
.util
.ArrayList
;
37 import java
.util
.List
;
41 * Adapts maps to either JSON objects or JSON arrays.
43 * <h3>Maps as JSON objects</h3>
44 * For primitive keys or when complex map key serialization is not enabled, this
45 * converts Java {@link Map Maps} to JSON Objects. This requires that map keys
46 * can be serialized as strings; this is insufficient for some key types. For
47 * example, consider a map whose keys are points on a grid. The default JSON
48 * form encodes reasonably: <pre> {@code
49 * Map<Point, String> original = new LinkedHashMap<Point, String>();
50 * original.put(new Point(5, 6), "a");
51 * original.put(new Point(8, 8), "b");
52 * System.out.println(gson.toJson(original, type));
54 * The above code prints this JSON object:<pre> {@code
60 * But GSON is unable to deserialize this value because the JSON string name is
61 * just the {@link Object#toString() toString()} of the map key. Attempting to
62 * convert the above JSON to an object fails with a parse exception:
63 * <pre>com.google.gson.JsonParseException: Expecting object found: "(5,6)"
64 * at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler
65 * at com.google.gson.ObjectNavigator.navigateClassFields
68 * <h3>Maps as JSON arrays</h3>
69 * An alternative approach taken by this type adapter when it is required and
70 * complex map key serialization is enabled is to encode maps as arrays of map
71 * entries. Each map entry is a two element array containing a key and a value.
72 * This approach is more flexible because any type can be used as the map's key;
73 * not just strings. But it's also less portable because the receiver of such
74 * JSON must be aware of the map entry convention.
76 * <p>Register this adapter when you are creating your GSON instance.
78 * Gson gson = new GsonBuilder()
79 * .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
82 * This will change the structure of the JSON emitted by the code above. Now we
83 * get an array. In this case the arrays elements are map entries:
102 * This format will serialize and deserialize just fine as long as this adapter
105 public final class MapTypeAdapterFactory
implements TypeAdapterFactory
{
106 private final ConstructorConstructor constructorConstructor
;
107 private final boolean complexMapKeySerialization
;
109 public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor
,
110 boolean complexMapKeySerialization
) {
111 this.constructorConstructor
= constructorConstructor
;
112 this.complexMapKeySerialization
= complexMapKeySerialization
;
115 public <T
> TypeAdapter
<T
> create(Gson gson
, TypeToken
<T
> typeToken
) {
116 Type type
= typeToken
.getType();
118 Class
<?
super T
> rawType
= typeToken
.getRawType();
119 if (!Map
.class.isAssignableFrom(rawType
)) {
123 Class
<?
> rawTypeOfSrc
= $Gson$Types
.getRawType(type
);
124 Type
[] keyAndValueTypes
= $Gson$Types
.getMapKeyAndValueTypes(type
, rawTypeOfSrc
);
125 TypeAdapter
<?
> keyAdapter
= getKeyAdapter(gson
, keyAndValueTypes
[0]);
126 TypeAdapter
<?
> valueAdapter
= gson
.getAdapter(TypeToken
.get(keyAndValueTypes
[1]));
127 ObjectConstructor
<T
> constructor
= constructorConstructor
.get(typeToken
);
129 @SuppressWarnings({"unchecked", "rawtypes"})
130 // we don't define a type parameter for the key or value types
131 TypeAdapter
<T
> result
= new Adapter(gson
, keyAndValueTypes
[0], keyAdapter
,
132 keyAndValueTypes
[1], valueAdapter
, constructor
);
137 * Returns a type adapter that writes the value as a string.
139 private TypeAdapter
<?
> getKeyAdapter(Gson context
, Type keyType
) {
140 return (keyType
== boolean.class || keyType
== Boolean
.class)
141 ? TypeAdapters
.BOOLEAN_AS_STRING
142 : context
.getAdapter(TypeToken
.get(keyType
));
145 private final class Adapter
<K
, V
> extends TypeAdapter
<Map
<K
, V
>> {
146 private final TypeAdapter
<K
> keyTypeAdapter
;
147 private final TypeAdapter
<V
> valueTypeAdapter
;
148 private final ObjectConstructor
<?
extends Map
<K
, V
>> constructor
;
150 public Adapter(Gson context
, Type keyType
, TypeAdapter
<K
> keyTypeAdapter
,
151 Type valueType
, TypeAdapter
<V
> valueTypeAdapter
,
152 ObjectConstructor
<?
extends Map
<K
, V
>> constructor
) {
153 this.keyTypeAdapter
=
154 new TypeAdapterRuntimeTypeWrapper
<K
>(context
, keyTypeAdapter
, keyType
);
155 this.valueTypeAdapter
=
156 new TypeAdapterRuntimeTypeWrapper
<V
>(context
, valueTypeAdapter
, valueType
);
157 this.constructor
= constructor
;
160 public Map
<K
, V
> read(JsonReader in
) throws IOException
{
161 JsonToken peek
= in
.peek();
162 if (peek
== JsonToken
.NULL
) {
167 Map
<K
, V
> map
= constructor
.construct();
169 if (peek
== JsonToken
.BEGIN_ARRAY
) {
171 while (in
.hasNext()) {
172 in
.beginArray(); // entry array
173 K key
= keyTypeAdapter
.read(in
);
174 V value
= valueTypeAdapter
.read(in
);
175 V replaced
= map
.put(key
, value
);
176 if (replaced
!= null) {
177 throw new JsonSyntaxException("duplicate key: " + key
);
184 while (in
.hasNext()) {
185 JsonReaderInternalAccess
.INSTANCE
.promoteNameToValue(in
);
186 K key
= keyTypeAdapter
.read(in
);
187 V value
= valueTypeAdapter
.read(in
);
188 V replaced
= map
.put(key
, value
);
189 if (replaced
!= null) {
190 throw new JsonSyntaxException("duplicate key: " + key
);
198 public void write(JsonWriter out
, Map
<K
, V
> map
) throws IOException
{
204 if (!complexMapKeySerialization
) {
206 for (Map
.Entry
<K
, V
> entry
: map
.entrySet()) {
207 out
.name(String
.valueOf(entry
.getKey()));
208 valueTypeAdapter
.write(out
, entry
.getValue());
214 boolean hasComplexKeys
= false;
215 List
<JsonElement
> keys
= new ArrayList
<JsonElement
>(map
.size());
217 List
<V
> values
= new ArrayList
<V
>(map
.size());
218 for (Map
.Entry
<K
, V
> entry
: map
.entrySet()) {
219 JsonElement keyElement
= keyTypeAdapter
.toJsonTree(entry
.getKey());
220 keys
.add(keyElement
);
221 values
.add(entry
.getValue());
222 hasComplexKeys
|= keyElement
.isJsonArray() || keyElement
.isJsonObject();
225 if (hasComplexKeys
) {
227 for (int i
= 0; i
< keys
.size(); i
++) {
228 out
.beginArray(); // entry array
229 Streams
.write(keys
.get(i
), out
);
230 valueTypeAdapter
.write(out
, values
.get(i
));
236 for (int i
= 0; i
< keys
.size(); i
++) {
237 JsonElement keyElement
= keys
.get(i
);
238 out
.name(keyToString(keyElement
));
239 valueTypeAdapter
.write(out
, values
.get(i
));
245 private String
keyToString(JsonElement keyElement
) {
246 if (keyElement
.isJsonPrimitive()) {
247 JsonPrimitive primitive
= keyElement
.getAsJsonPrimitive();
248 if (primitive
.isNumber()) {
249 return String
.valueOf(primitive
.getAsNumber());
250 } else if (primitive
.isBoolean()) {
251 return Boolean
.toString(primitive
.getAsBoolean());
252 } else if (primitive
.isString()) {
253 return primitive
.getAsString();
255 throw new AssertionError();
257 } else if (keyElement
.isJsonNull()) {
260 throw new AssertionError();