Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -220,8 +222,15 @@ public void write(JsonWriter out, Map<K, V> map) throws IOException {

if (!complexMapKeySerialization) {
out.beginObject();
Set<String> emittedNames = new HashSet<>();
for (Map.Entry<K, V> 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();
Expand Down Expand Up @@ -250,9 +259,14 @@ public void write(JsonWriter out, Map<K, V> map) throws IOException {
out.endArray();
} else {
out.beginObject();
Set<String> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to also cover the complex map key scenario which you changed as well:

  • a case where complex map keys are enabled, the keys serialize to a complex value (possibly identical for all keys, e.g. ["k"]), toString() is identical (e.g. "k") but the duplicate keys are not rejected
  • a case where complex map keys are enabled, but all keys serialize to a non-complex value, and duplicate keys are rejected

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are 100% right – I hyperfocused on a corner case. Thank you! Closing this PR.

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<Object, String> 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<Map<Object, String>>() {}.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);
Comment on lines +40 to +56

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If throwing an exception is the desired and expected behavior now, then the test should probably reflect that and use assertThrows?

}
}