diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 62d5ca86..b6e714da 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -110,6 +110,7 @@ public final class Gson { static final boolean DEFAULT_ESCAPE_HTML = true; static final boolean DEFAULT_SERIALIZE_NULLS = false; static final boolean DEFAULT_COMPLEX_MAP_KEYS = false; + static final boolean DEFAULT_DUPLICATE_MAP_KEYS = false; static final boolean DEFAULT_SPECIALIZE_FLOAT_VALUES = false; static final boolean DEFAULT_USE_JDK_UNSAFE = true; static final String DEFAULT_DATE_PATTERN = null; @@ -142,6 +143,7 @@ public final class Gson { final Map> instanceCreators; final boolean serializeNulls; final boolean complexMapKeySerialization; + final boolean duplicateMapKeyDeserialization; final boolean generateNonExecutableJson; final boolean htmlSafe; final boolean prettyPrinting; @@ -195,7 +197,7 @@ public final class Gson { public Gson() { this(Excluder.DEFAULT, DEFAULT_FIELD_NAMING_STRATEGY, Collections.>emptyMap(), DEFAULT_SERIALIZE_NULLS, - DEFAULT_COMPLEX_MAP_KEYS, DEFAULT_JSON_NON_EXECUTABLE, DEFAULT_ESCAPE_HTML, + DEFAULT_COMPLEX_MAP_KEYS, DEFAULT_DUPLICATE_MAP_KEYS, DEFAULT_JSON_NON_EXECUTABLE, DEFAULT_ESCAPE_HTML, DEFAULT_PRETTY_PRINT, DEFAULT_LENIENT, DEFAULT_SPECIALIZE_FLOAT_VALUES, DEFAULT_USE_JDK_UNSAFE, LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT, @@ -206,7 +208,7 @@ public final class Gson { Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy, Map> instanceCreators, boolean serializeNulls, - boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe, + boolean complexMapKeySerialization, boolean duplicateMapKeyDeserialization, boolean generateNonExecutableGson, boolean htmlSafe, boolean prettyPrinting, boolean lenient, boolean serializeSpecialFloatingPointValues, boolean useJdkUnsafe, LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle, @@ -221,6 +223,7 @@ public final class Gson { this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe, reflectionFilters); this.serializeNulls = serializeNulls; this.complexMapKeySerialization = complexMapKeySerialization; + this.duplicateMapKeyDeserialization = duplicateMapKeyDeserialization; this.generateNonExecutableJson = generateNonExecutableGson; this.htmlSafe = htmlSafe; this.prettyPrinting = prettyPrinting; @@ -295,7 +298,7 @@ public final class Gson { // type adapters for composite and user-defined types factories.add(new CollectionTypeAdapterFactory(constructorConstructor)); - factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization)); + factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization, duplicateMapKeyDeserialization)); this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor); factories.add(jsonAdapterFactory); factories.add(TypeAdapters.ENUM_FACTORY); diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 5e77ac0c..bc97aec5 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -37,6 +37,7 @@ import com.google.gson.stream.JsonReader; import static com.google.gson.Gson.DEFAULT_COMPLEX_MAP_KEYS; import static com.google.gson.Gson.DEFAULT_DATE_PATTERN; +import static com.google.gson.Gson.DEFAULT_DUPLICATE_MAP_KEYS; import static com.google.gson.Gson.DEFAULT_ESCAPE_HTML; import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE; import static com.google.gson.Gson.DEFAULT_LENIENT; @@ -93,6 +94,7 @@ public final class GsonBuilder { private int dateStyle = DateFormat.DEFAULT; private int timeStyle = DateFormat.DEFAULT; private boolean complexMapKeySerialization = DEFAULT_COMPLEX_MAP_KEYS; + private boolean duplicateMapKeyDeserialization = DEFAULT_DUPLICATE_MAP_KEYS; private boolean serializeSpecialFloatingPointValues = DEFAULT_SPECIALIZE_FLOAT_VALUES; private boolean escapeHtmlChars = DEFAULT_ESCAPE_HTML; private boolean prettyPrinting = DEFAULT_PRETTY_PRINT; @@ -124,6 +126,7 @@ public final class GsonBuilder { this.instanceCreators.putAll(gson.instanceCreators); this.serializeNulls = gson.serializeNulls; this.complexMapKeySerialization = gson.complexMapKeySerialization; + this.duplicateMapKeyDeserialization = gson.duplicateMapKeyDeserialization; this.generateNonExecutableJson = gson.generateNonExecutableJson; this.escapeHtmlChars = gson.htmlSafe; this.prettyPrinting = gson.prettyPrinting; @@ -288,6 +291,22 @@ public final class GsonBuilder { return this; } + /** + * Configures Gson to deserialize duplicate map keys. Only the value of last entry with the same key will be used, previous values + * will be discarded. By default, Gson throws a {@link JsonSyntaxException} when a key occurs more than once. + * + *

Note that enabling support for duplicate maps keys is discouraged because it can make an application less secure. + * When an application interacts with other components using different JSON libraries, they might treat duplicate keys + * differently, allowing an attacker to circumvent security checks. + * + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 2.8 + */ + public GsonBuilder enableDuplicateMapKeyDeserialization() { + duplicateMapKeyDeserialization = true; + return this; + } + /** * Configures Gson to exclude inner classes during serialization. * @@ -674,7 +693,7 @@ public final class GsonBuilder { addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories); return new Gson(excluder, fieldNamingPolicy, new HashMap<>(instanceCreators), - serializeNulls, complexMapKeySerialization, + serializeNulls, complexMapKeySerialization, duplicateMapKeyDeserialization, generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient, serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy, datePattern, dateStyle, timeStyle, new ArrayList<>(this.factories), diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java index f7c5a554..166bae0f 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java @@ -105,11 +105,13 @@ import java.util.Map; public final class MapTypeAdapterFactory implements TypeAdapterFactory { private final ConstructorConstructor constructorConstructor; final boolean complexMapKeySerialization; + final boolean duplicateMapKeyDeserialization; public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor, - boolean complexMapKeySerialization) { + boolean complexMapKeySerialization, boolean duplicateMapKeyDeserialization) { this.constructorConstructor = constructorConstructor; this.complexMapKeySerialization = complexMapKeySerialization; + this.duplicateMapKeyDeserialization = duplicateMapKeyDeserialization; } @Override public TypeAdapter create(Gson gson, TypeToken typeToken) { @@ -172,7 +174,7 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory { K key = keyTypeAdapter.read(in); V value = valueTypeAdapter.read(in); V replaced = map.put(key, value); - if (replaced != null) { + if (!duplicateMapKeyDeserialization && replaced != null) { throw new JsonSyntaxException("duplicate key: " + key); } in.endArray(); @@ -185,7 +187,7 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory { K key = keyTypeAdapter.read(in); V value = valueTypeAdapter.read(in); V replaced = map.put(key, value); - if (replaced != null) { + if (!duplicateMapKeyDeserialization && replaced != null) { throw new JsonSyntaxException("duplicate key: " + key); } } diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index abb0de21..3e002369 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -53,7 +53,7 @@ public final class GsonTest extends TestCase { public void testOverridesDefaultExcluder() { Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, - new HashMap>(), true, false, true, false, + new HashMap>(), true, false, false, true, false, true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), @@ -68,7 +68,7 @@ public final class GsonTest extends TestCase { public void testClonedTypeAdapterFactoryListsAreIndependent() { Gson original = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, - new HashMap>(), true, false, true, false, + new HashMap>(), true, false, false, true, false, true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), diff --git a/gson/src/test/java/com/google/gson/functional/MapTest.java b/gson/src/test/java/com/google/gson/functional/MapTest.java index ef9eae2b..b921bd4f 100644 --- a/gson/src/test/java/com/google/gson/functional/MapTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -78,6 +78,17 @@ public class MapTest extends TestCase { assertEquals(2, target.get("b").intValue()); } + public void testMapDuplicateKeyDeserialization() { + Gson gsonWithDuplicateKeys = new GsonBuilder() + .enableDuplicateMapKeyDeserialization() + .create(); + Type typeOfMap = new TypeToken>(){}.getType(); + String json = "{\"a\":1,\"b\":2,\"b\":3}"; + Map target = gsonWithDuplicateKeys.fromJson(json, typeOfMap); + assertEquals(1, target.get("a").intValue()); + assertEquals(3, target.get("b").intValue()); + } + @SuppressWarnings({"unchecked", "rawtypes"}) public void testRawMapSerialization() { Map map = new LinkedHashMap();