From 9db736af5ab9ab601e97ba1864f42d6812d2f024 Mon Sep 17 00:00:00 2001 From: andrewstellman Date: Sat, 6 Jun 2026 01:52:43 -0400 Subject: [PATCH 1/2] test: add failing test for Map serialization duplicate-key emission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two distinct Map keys whose String.valueOf forms collide silently produce a JSON object with duplicate member names — which the read path rejects with 'duplicate key' on the next deserialize. The write/read asymmetry violates the symmetry contract. This commit adds the failing regression test demonstrating the write-side dup-key emission. --- .../MapTypeAdapterDuplicateKeyTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 gson/src/test/java/com/google/gson/regression/MapTypeAdapterDuplicateKeyTest.java diff --git a/gson/src/test/java/com/google/gson/regression/MapTypeAdapterDuplicateKeyTest.java b/gson/src/test/java/com/google/gson/regression/MapTypeAdapterDuplicateKeyTest.java new file mode 100644 index 0000000000..5a83fc75e5 --- /dev/null +++ b/gson/src/test/java/com/google/gson/regression/MapTypeAdapterDuplicateKeyTest.java @@ -0,0 +1,58 @@ +// Generated by Quality Playbook v1.5.8 — https://github.com/andrewstellman/quality-playbook +// Author: Andrew Stellman · Date: 2026-06-04 · Project: gson +package com.google.gson.regression; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; + +/** Regression test: map serialization must not silently emit duplicate JSON member names. */ +public class MapTypeAdapterDuplicateKeyTest { + @Test + public void mapWriteDoesNotEmitDuplicateNames() { + Gson gson = new Gson(); + Object k1 = + new Object() { + @Override + public String toString() { + return "k"; + } + }; + Object k2 = + new Object() { + @Override + public String toString() { + return "k"; + } + }; + Map map = new LinkedHashMap<>(); + map.put(k1, "a"); + map.put(k2, "b"); + // Confirm two distinct anonymous-instance keys (defensive against future key-type refactors). + assertThat(map).hasSize(2); + + String json; + try { + json = gson.toJson(map, new TypeToken>() {}.getType()); + } catch (JsonSyntaxException expected) { + // Acceptable fix: throw JsonSyntaxException naming the duplicate key. Narrow match — + // message must reference "duplicate key:" followed by the literal colliding name "k", + // not merely contain the word "key" from any unrelated JSON syntax error. + assertThat(expected).hasMessageThat().containsMatch("duplicate key:\\s*k(?:\\W|$)"); + return; + } + + // Otherwise: gson emitted JSON without throwing. Pin the object-form serialization (not the + // complex-key array-form) so the indexOf "k": substring check is exercising the bug's locus. + assertThat(json).startsWith("{"); + int first = json.indexOf("\"k\":"); + assertThat(first).isAtLeast(0); + int second = json.indexOf("\"k\":", first + 1); + assertThat(second).isEqualTo(-1); + } +} From d782fd59af07e0cf8fa822b19aae0de829b00171 Mon Sep 17 00:00:00 2001 From: andrewstellman Date: Sat, 6 Jun 2026 01:53:41 -0400 Subject: [PATCH 2/2] fix: MapTypeAdapterFactory rejects duplicate keys on write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The map read path already throws JsonSyntaxException('duplicate key: ...') on collisions. The write path didn't track emitted names — so Map.entrySet iteration of two keys whose String.valueOf form collides silently produced a JSON object with duplicate member names that the same Gson instance would refuse to read. Mirror the read path's check: track emitted name strings in a HashSet and throw JsonSyntaxException on the second occurrence. Applies to both the non-complex object path and the complex-array-fallback object path. This is a sibling-bug counterpart to the read-side dup-key fix in f4d371d (#3006). Existing MapTest, JsonTreeWriterTest suites pass. --- .../internal/bind/MapTypeAdapterFactory.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) 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 a72bb48cc1..b285665338 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 @@ -34,8 +34,10 @@ import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Adapts maps to either JSON objects or JSON arrays. @@ -220,8 +222,15 @@ public void write(JsonWriter out, Map map) throws IOException { if (!complexMapKeySerialization) { out.beginObject(); + Set emittedNames = new HashSet<>(); for (Map.Entry entry : map.entrySet()) { - out.name(String.valueOf(entry.getKey())); + String name = String.valueOf(entry.getKey()); + // Mirror the read path's duplicate-key rejection: distinct keys whose string form + // collides would otherwise silently emit a duplicate JSON object member name. + if (!emittedNames.add(name)) { + throw new JsonSyntaxException("duplicate key: " + name); + } + out.name(name); valueTypeAdapter.write(out, entry.getValue()); } out.endObject(); @@ -250,9 +259,14 @@ public void write(JsonWriter out, Map map) throws IOException { out.endArray(); } else { out.beginObject(); + Set emittedNames = new HashSet<>(); for (int i = 0, size = keys.size(); i < size; i++) { JsonElement keyElement = keys.get(i); - out.name(keyToString(keyElement)); + String name = keyToString(keyElement); + if (!emittedNames.add(name)) { + throw new JsonSyntaxException("duplicate key: " + name); + } + out.name(name); valueTypeAdapter.write(out, values.get(i)); } out.endObject();