From ed7e9b107ed48f7d5e59f145c324cf47e057ed7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 13 Jul 2026 14:53:31 -0700 Subject: [PATCH 1/6] Use LinkedHashMap as the default Map. The JDK and Android have had DoS protection at least as good as LinkedTreeMap for decades now. --- .../google/gson/internal/ConstructorConstructor.java | 11 +---------- .../google/gson/internal/bind/ObjectTypeAdapter.java | 4 ++-- .../test/java/com/google/gson/functional/MapTest.java | 6 ------ .../gson/internal/ConstructorConstructorTest.java | 7 ++----- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java index 43d9947503..18caeef3fc 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -352,16 +352,7 @@ private static boolean hasStringKeyType(Type mapType) { private static ObjectConstructor> newMapConstructor( Type type, Class rawType) { // First try Map implementation - /* - * Legacy special casing for Map to avoid DoS from colliding String hashCode - * values for older JDKs; use own LinkedTreeMap instead - */ - if (rawType.isAssignableFrom(LinkedTreeMap.class) && hasStringKeyType(type)) { - // Must use lambda instead of method reference (`LinkedTreeMap::new`) here, otherwise this - // causes an exception when Gson is used by a custom system class loader, see - // https://github.com/google/gson/pull/2864#issuecomment-3528623716 - return () -> new LinkedTreeMap<>(); - } else if (rawType.isAssignableFrom(LinkedHashMap.class)) { + if (rawType.isAssignableFrom(LinkedHashMap.class)) { return LinkedHashMap::new; } // Then try SortedMap / NavigableMap implementation diff --git a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java index 2fa704544f..e08c41a46e 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java @@ -21,7 +21,6 @@ import com.google.gson.ToNumberStrategy; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.LinkedTreeMap; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; @@ -30,6 +29,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -81,7 +81,7 @@ private Object tryBeginNesting(JsonReader in, JsonToken peeked) throws IOExcepti return new ArrayList<>(); case BEGIN_OBJECT: in.beginObject(); - return new LinkedTreeMap<>(); + return new LinkedHashMap<>(); default: return null; } 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 4c0d046d54..490544d571 100644 --- a/gson/src/test/java/com/google/gson/functional/MapTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -220,12 +220,6 @@ public void testMapStringKeyDeserialization() { Type typeOfMap = new TypeToken>() {}.getType(); Map map = gson.fromJson("{\"a\":1}", typeOfMap); - assertWithMessage( - "Map should use LinkedTreeMap to protect against DoS in older JDK" - + " versions") - .that(map) - .isInstanceOf(LinkedTreeMap.class); - Map expectedMap = Collections.singletonMap("a", 1); assertThat(map).isEqualTo(expectedMap); } diff --git a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java index 102eaf7e06..2b5ed1771b 100644 --- a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java +++ b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java @@ -158,15 +158,12 @@ public void testCustomCollectionInterfaceCreation() { @Test public void testStringMapCreation() { - // When creating raw Map should use Gson's LinkedTreeMap, assuming keys could be String Object actual = constructorConstructor.get(TypeToken.get(Map.class)).construct(); - assertThat(actual).isInstanceOf(LinkedTreeMap.class); + assertThat(actual).isInstanceOf(LinkedHashMap.class); - // When creating a `Map` should use Gson's LinkedTreeMap actual = constructorConstructor.get(new TypeToken>() {}).construct(); - assertThat(actual).isInstanceOf(LinkedTreeMap.class); + assertThat(actual).isInstanceOf(LinkedHashMap.class); - // But when explicitly requesting a JDK `LinkedHashMap` should use LinkedHashMap actual = constructorConstructor.get(new TypeToken>() {}).construct(); assertThat(actual).isInstanceOf(LinkedHashMap.class); From bf3973a851cfcbf449e2dc4e633256f17c1fe4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 13 Jul 2026 14:57:59 -0700 Subject: [PATCH 2/6] Add a test for deserializing a null key in the list-of-entries encoding. --- .../src/test/java/com/google/gson/functional/MapTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 490544d571..58c1ae3278 100644 --- a/gson/src/test/java/com/google/gson/functional/MapTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -165,6 +165,14 @@ public void testMapDeserializationWithNullKey() { assertThat(map.get(null)).isNull(); } + @Test + public void testMapDeserializationWithNullKeyAndListOfEntries() { + Type typeOfMap = new TypeToken>() {}.getType(); + Map map = gson.fromJson("[[null,123]]", typeOfMap); + assertThat(map).hasSize(1); + assertThat(map.get(null)).isEqualTo(123); + } + @Test public void testMapSerializationWithIntegerKeys() { Map map = new LinkedHashMap<>(); From 8dc85ff4e705edd17fff8185dc13e8f5f37d6a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 13 Jul 2026 15:04:46 -0700 Subject: [PATCH 3/6] Fix 'unused' warnings. --- .../gson/internal/ConstructorConstructor.java | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java index 18caeef3fc..f67d2e99cd 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -304,7 +304,7 @@ private static ObjectConstructor newDefaultImplementationConstructor( if (Map.class.isAssignableFrom(rawType)) { @SuppressWarnings("unchecked") - ObjectConstructor constructor = (ObjectConstructor) newMapConstructor(type, rawType); + ObjectConstructor constructor = (ObjectConstructor) newMapConstructor(rawType); return constructor; } @@ -336,21 +336,7 @@ else if (rawType.isAssignableFrom(ArrayDeque.class)) { return null; } - private static boolean hasStringKeyType(Type mapType) { - // If mapType is not parameterized, assume it might have String as key type - if (!(mapType instanceof ParameterizedType)) { - return true; - } - - Type[] typeArguments = ((ParameterizedType) mapType).getActualTypeArguments(); - if (typeArguments.length == 0) { - return false; - } - return GsonTypes.getRawType(typeArguments[0]) == String.class; - } - - private static ObjectConstructor> newMapConstructor( - Type type, Class rawType) { + private static ObjectConstructor> newMapConstructor(Class rawType) { // First try Map implementation if (rawType.isAssignableFrom(LinkedHashMap.class)) { return LinkedHashMap::new; From 56b4be6f7a72e95324ca17889dd14d40d75d4eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 13 Jul 2026 15:07:04 -0700 Subject: [PATCH 4/6] Another 'unused' warning. --- .../java/com/google/gson/internal/ConstructorConstructor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java index f67d2e99cd..843451e8d5 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -134,7 +134,7 @@ public ObjectConstructor get(TypeToken typeToken, boolean allowUnsafe) return defaultConstructor; } - ObjectConstructor defaultImplementation = newDefaultImplementationConstructor(type, rawType); + ObjectConstructor defaultImplementation = newDefaultImplementationConstructor(rawType); if (defaultImplementation != null) { return defaultImplementation; } @@ -286,7 +286,7 @@ private static ObjectConstructor newDefaultConstructor( /** Constructors for common interface types like Map and List and their subtypes. */ private static ObjectConstructor newDefaultImplementationConstructor( - Type type, Class rawType) { + Class rawType) { /* * IMPORTANT: Must only create instances for classes with public no-args constructor. From 2374e7b9aea6f6998431580509e2a9dd84b925ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 20 Jul 2026 17:45:03 -0700 Subject: [PATCH 5/6] Respond to review comments. --- .../com/google/gson/functional/MapTest.java | 27 ------------------- .../internal/ConstructorConstructorTest.java | 4 +-- 2 files changed, 1 insertion(+), 30 deletions(-) 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 58c1ae3278..29d3fabcf0 100644 --- a/gson/src/test/java/com/google/gson/functional/MapTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -232,33 +232,6 @@ public void testMapStringKeyDeserialization() { assertThat(map).isEqualTo(expectedMap); } - @Test - public void testMapStringSupertypeKeyDeserialization() { - // Should only use Gson's LinkedTreeMap for String as key, but not for supertypes (e.g. Object) - Type typeOfMap = new TypeToken>() {}.getType(); - Map map = gson.fromJson("{\"a\":1}", typeOfMap); - - assertWithMessage("Map should not use Gson Map implementation") - .that(map) - .isNotInstanceOf(LinkedTreeMap.class); - - Map expectedMap = Collections.singletonMap("a", 1); - assertThat(map).isEqualTo(expectedMap); - } - - @Test - public void testMapNonStringKeyDeserialization() { - Type typeOfMap = new TypeToken>() {}.getType(); - Map map = gson.fromJson("{\"1\":1}", typeOfMap); - - assertWithMessage("Map should not use Gson Map implementation") - .that(map) - .isNotInstanceOf(LinkedTreeMap.class); - - Map expectedMap = Collections.singletonMap(1, 1); - assertThat(map).isEqualTo(expectedMap); - } - @Test public void testHashMapDeserialization() { Type typeOfMap = new TypeToken>() {}.getType(); diff --git a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java index 2b5ed1771b..10fcc88e41 100644 --- a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java +++ b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java @@ -157,7 +157,7 @@ public void testCustomCollectionInterfaceCreation() { } @Test - public void testStringMapCreation() { + public void testMapCreation() { Object actual = constructorConstructor.get(TypeToken.get(Map.class)).construct(); assertThat(actual).isInstanceOf(LinkedHashMap.class); @@ -169,8 +169,6 @@ public void testStringMapCreation() { assertThat(actual).isInstanceOf(LinkedHashMap.class); // For all Map types with non-String key, should use JDK LinkedHashMap by default - // This is also done to avoid ClassCastException later, because Gson's LinkedTreeMap requires - // that keys are Comparable Class[] nonStringTypes = {Integer.class, CharSequence.class, Object.class}; for (Class keyType : nonStringTypes) { actual = From a26350290674b408be13907024b5463c194e319e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 20 Jul 2026 17:48:08 -0700 Subject: [PATCH 6/6] Remove unused imports. --- gson/src/test/java/com/google/gson/functional/MapTest.java | 2 -- 1 file changed, 2 deletions(-) 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 29d3fabcf0..4a7dfb97ed 100644 --- a/gson/src/test/java/com/google/gson/functional/MapTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapTest.java @@ -17,7 +17,6 @@ package com.google.gson.functional; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import com.google.gson.Gson; @@ -33,7 +32,6 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.common.TestTypes; import com.google.gson.internal.GsonTypes; -import com.google.gson.internal.LinkedTreeMap; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.AbstractMap;