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(); 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); + } +}