Merge branch 'allow_duplicate_map_key'

# Conflicts:
#	gson/src/main/java/com/google/gson/GsonBuilder.java
#	gson/src/test/java/com/google/gson/GsonTest.java
This commit is contained in:
Johannes Frohnmeyer 2022-05-12 22:54:17 +02:00
commit 9d7fd891ee
Signed by: Johannes
GPG Key ID: E76429612C2929F4
5 changed files with 44 additions and 9 deletions

View File

@ -110,6 +110,7 @@ public final class Gson {
static final boolean DEFAULT_ESCAPE_HTML = true; static final boolean DEFAULT_ESCAPE_HTML = true;
static final boolean DEFAULT_SERIALIZE_NULLS = false; static final boolean DEFAULT_SERIALIZE_NULLS = false;
static final boolean DEFAULT_COMPLEX_MAP_KEYS = 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_SPECIALIZE_FLOAT_VALUES = false;
static final boolean DEFAULT_USE_JDK_UNSAFE = true; static final boolean DEFAULT_USE_JDK_UNSAFE = true;
static final String DEFAULT_DATE_PATTERN = null; static final String DEFAULT_DATE_PATTERN = null;
@ -142,6 +143,7 @@ public final class Gson {
final Map<Type, InstanceCreator<?>> instanceCreators; final Map<Type, InstanceCreator<?>> instanceCreators;
final boolean serializeNulls; final boolean serializeNulls;
final boolean complexMapKeySerialization; final boolean complexMapKeySerialization;
final boolean duplicateMapKeyDeserialization;
final boolean generateNonExecutableJson; final boolean generateNonExecutableJson;
final boolean htmlSafe; final boolean htmlSafe;
final boolean prettyPrinting; final boolean prettyPrinting;
@ -195,7 +197,7 @@ public final class Gson {
public Gson() { public Gson() {
this(Excluder.DEFAULT, DEFAULT_FIELD_NAMING_STRATEGY, this(Excluder.DEFAULT, DEFAULT_FIELD_NAMING_STRATEGY,
Collections.<Type, InstanceCreator<?>>emptyMap(), DEFAULT_SERIALIZE_NULLS, Collections.<Type, InstanceCreator<?>>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_PRETTY_PRINT, DEFAULT_LENIENT, DEFAULT_SPECIALIZE_FLOAT_VALUES,
DEFAULT_USE_JDK_UNSAFE, DEFAULT_USE_JDK_UNSAFE,
LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT, LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT,
@ -206,7 +208,7 @@ public final class Gson {
Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy, Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy,
Map<Type, InstanceCreator<?>> instanceCreators, boolean serializeNulls, Map<Type, InstanceCreator<?>> instanceCreators, boolean serializeNulls,
boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe, boolean complexMapKeySerialization, boolean duplicateMapKeyDeserialization, boolean generateNonExecutableGson, boolean htmlSafe,
boolean prettyPrinting, boolean lenient, boolean serializeSpecialFloatingPointValues, boolean prettyPrinting, boolean lenient, boolean serializeSpecialFloatingPointValues,
boolean useJdkUnsafe, boolean useJdkUnsafe,
LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle, LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle,
@ -221,6 +223,7 @@ public final class Gson {
this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe, reflectionFilters); this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe, reflectionFilters);
this.serializeNulls = serializeNulls; this.serializeNulls = serializeNulls;
this.complexMapKeySerialization = complexMapKeySerialization; this.complexMapKeySerialization = complexMapKeySerialization;
this.duplicateMapKeyDeserialization = duplicateMapKeyDeserialization;
this.generateNonExecutableJson = generateNonExecutableGson; this.generateNonExecutableJson = generateNonExecutableGson;
this.htmlSafe = htmlSafe; this.htmlSafe = htmlSafe;
this.prettyPrinting = prettyPrinting; this.prettyPrinting = prettyPrinting;
@ -295,7 +298,7 @@ public final class Gson {
// type adapters for composite and user-defined types // type adapters for composite and user-defined types
factories.add(new CollectionTypeAdapterFactory(constructorConstructor)); factories.add(new CollectionTypeAdapterFactory(constructorConstructor));
factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization)); factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization, duplicateMapKeyDeserialization));
this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor); this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor);
factories.add(jsonAdapterFactory); factories.add(jsonAdapterFactory);
factories.add(TypeAdapters.ENUM_FACTORY); factories.add(TypeAdapters.ENUM_FACTORY);

View File

@ -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_COMPLEX_MAP_KEYS;
import static com.google.gson.Gson.DEFAULT_DATE_PATTERN; 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_ESCAPE_HTML;
import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE; import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE;
import static com.google.gson.Gson.DEFAULT_LENIENT; import static com.google.gson.Gson.DEFAULT_LENIENT;
@ -93,6 +94,7 @@ public final class GsonBuilder {
private int dateStyle = DateFormat.DEFAULT; private int dateStyle = DateFormat.DEFAULT;
private int timeStyle = DateFormat.DEFAULT; private int timeStyle = DateFormat.DEFAULT;
private boolean complexMapKeySerialization = DEFAULT_COMPLEX_MAP_KEYS; private boolean complexMapKeySerialization = DEFAULT_COMPLEX_MAP_KEYS;
private boolean duplicateMapKeyDeserialization = DEFAULT_DUPLICATE_MAP_KEYS;
private boolean serializeSpecialFloatingPointValues = DEFAULT_SPECIALIZE_FLOAT_VALUES; private boolean serializeSpecialFloatingPointValues = DEFAULT_SPECIALIZE_FLOAT_VALUES;
private boolean escapeHtmlChars = DEFAULT_ESCAPE_HTML; private boolean escapeHtmlChars = DEFAULT_ESCAPE_HTML;
private boolean prettyPrinting = DEFAULT_PRETTY_PRINT; private boolean prettyPrinting = DEFAULT_PRETTY_PRINT;
@ -124,6 +126,7 @@ public final class GsonBuilder {
this.instanceCreators.putAll(gson.instanceCreators); this.instanceCreators.putAll(gson.instanceCreators);
this.serializeNulls = gson.serializeNulls; this.serializeNulls = gson.serializeNulls;
this.complexMapKeySerialization = gson.complexMapKeySerialization; this.complexMapKeySerialization = gson.complexMapKeySerialization;
this.duplicateMapKeyDeserialization = gson.duplicateMapKeyDeserialization;
this.generateNonExecutableJson = gson.generateNonExecutableJson; this.generateNonExecutableJson = gson.generateNonExecutableJson;
this.escapeHtmlChars = gson.htmlSafe; this.escapeHtmlChars = gson.htmlSafe;
this.prettyPrinting = gson.prettyPrinting; this.prettyPrinting = gson.prettyPrinting;
@ -288,6 +291,22 @@ public final class GsonBuilder {
return this; 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.
*
* <p>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. * Configures Gson to exclude inner classes during serialization.
* *
@ -674,7 +693,7 @@ public final class GsonBuilder {
addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories); addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories);
return new Gson(excluder, fieldNamingPolicy, new HashMap<>(instanceCreators), return new Gson(excluder, fieldNamingPolicy, new HashMap<>(instanceCreators),
serializeNulls, complexMapKeySerialization, serializeNulls, complexMapKeySerialization, duplicateMapKeyDeserialization,
generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient, generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient,
serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy, serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy,
datePattern, dateStyle, timeStyle, new ArrayList<>(this.factories), datePattern, dateStyle, timeStyle, new ArrayList<>(this.factories),

View File

@ -105,11 +105,13 @@ import java.util.Map;
public final class MapTypeAdapterFactory implements TypeAdapterFactory { public final class MapTypeAdapterFactory implements TypeAdapterFactory {
private final ConstructorConstructor constructorConstructor; private final ConstructorConstructor constructorConstructor;
final boolean complexMapKeySerialization; final boolean complexMapKeySerialization;
final boolean duplicateMapKeyDeserialization;
public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor, public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor,
boolean complexMapKeySerialization) { boolean complexMapKeySerialization, boolean duplicateMapKeyDeserialization) {
this.constructorConstructor = constructorConstructor; this.constructorConstructor = constructorConstructor;
this.complexMapKeySerialization = complexMapKeySerialization; this.complexMapKeySerialization = complexMapKeySerialization;
this.duplicateMapKeyDeserialization = duplicateMapKeyDeserialization;
} }
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
@ -172,7 +174,7 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory {
K key = keyTypeAdapter.read(in); K key = keyTypeAdapter.read(in);
V value = valueTypeAdapter.read(in); V value = valueTypeAdapter.read(in);
V replaced = map.put(key, value); V replaced = map.put(key, value);
if (replaced != null) { if (!duplicateMapKeyDeserialization && replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key); throw new JsonSyntaxException("duplicate key: " + key);
} }
in.endArray(); in.endArray();
@ -185,7 +187,7 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory {
K key = keyTypeAdapter.read(in); K key = keyTypeAdapter.read(in);
V value = valueTypeAdapter.read(in); V value = valueTypeAdapter.read(in);
V replaced = map.put(key, value); V replaced = map.put(key, value);
if (replaced != null) { if (!duplicateMapKeyDeserialization && replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key); throw new JsonSyntaxException("duplicate key: " + key);
} }
} }

View File

@ -53,7 +53,7 @@ public final class GsonTest extends TestCase {
public void testOverridesDefaultExcluder() { public void testOverridesDefaultExcluder() {
Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY,
new HashMap<Type, InstanceCreator<?>>(), true, false, true, false, new HashMap<Type, InstanceCreator<?>>(), true, false, false, true, false,
true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT,
DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(), DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(),
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(),
@ -68,7 +68,7 @@ public final class GsonTest extends TestCase {
public void testClonedTypeAdapterFactoryListsAreIndependent() { public void testClonedTypeAdapterFactoryListsAreIndependent() {
Gson original = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, Gson original = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY,
new HashMap<Type, InstanceCreator<?>>(), true, false, true, false, new HashMap<Type, InstanceCreator<?>>(), true, false, false, true, false,
true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT,
DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(), DateFormat.DEFAULT, new ArrayList<TypeAdapterFactory>(),
new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(), new ArrayList<TypeAdapterFactory>(),

View File

@ -78,6 +78,17 @@ public class MapTest extends TestCase {
assertEquals(2, target.get("b").intValue()); assertEquals(2, target.get("b").intValue());
} }
public void testMapDuplicateKeyDeserialization() {
Gson gsonWithDuplicateKeys = new GsonBuilder()
.enableDuplicateMapKeyDeserialization()
.create();
Type typeOfMap = new TypeToken<Map<String,Integer>>(){}.getType();
String json = "{\"a\":1,\"b\":2,\"b\":3}";
Map<String,Integer> target = gsonWithDuplicateKeys.fromJson(json, typeOfMap);
assertEquals(1, target.get("a").intValue());
assertEquals(3, target.get("b").intValue());
}
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
public void testRawMapSerialization() { public void testRawMapSerialization() {
Map map = new LinkedHashMap(); Map map = new LinkedHashMap();