Contents

Duplicate object names in JSON are a small syntax detail with outsized pipeline consequences. The input {"a":1,"a":2} is accepted by many parsers, but the value you observe can be 1, 2, an error, or a preserved pair depending on the receiver. This guide starts with the specification, then compares reported behavior across JavaScript, Python, Go, Java, .NET, PostgreSQL, and a verification harness for Rust. The goal is not to crown one parser as correct. It is to show where ambiguity enters your data path and how to detect it before a formatter, database, or service silently chooses a winner. Examples are runnable where shown; Rust output is intentionally withheld until verified.
Specification: SHOULD unique, not MUST
The specification is often summarized too strongly. RFC 8259 §4 says object names SHOULD be unique. SHOULD is not MUST: it is an interoperability recommendation, not a requirement that a parser reject duplicates or pick a particular value. The same section notes that when names are not unique, receiver behavior is unpredictable, and many implementations report only the last name/value pair. That is a warning about portability, not a rule that last-value-wins is correct.
ECMA-404, which defines the JSON syntax, does not require object member names to be unique. It describes objects as collections of name/value pairs; uniqueness is not part of the grammar. So a document with repeated names can be syntactically valid JSON and still be semantically ambiguous.
This gap is deliberate enough to matter. A validator can say “this is valid JSON” while a parser says “here is the value” and a different parser says “here is another value” or “duplicate key error.” If your application needs one value for one name, the safe rule is to enforce uniqueness before the data reaches a parser whose behavior you do not control. The spec’s SHOULD is best read as: send unique names; do not rely on the receiver’s duplicate policy.
Behavior taxonomy and why parsers differ
Four behaviors cover most implementations:
-
Last-value-wins. The parser builds a map or dictionary and assigns each pair in order. A later pair replaces the earlier value. Gson, Jackson ObjectMapper, JavaScript engines such as V8, Newtonsoft.Json, Ruby, Go encoding/json, and json_decode have appeared in the last-value-wins group. CPython json.loads has also been reported as last-value-wins, with the duplicate input re-serialised as
{"a": 2}. This is a default, not a spec mandate. -
First-value-wins. The parser keeps the first pair and ignores later ones. The evidence summarized here did not identify a mainstream parser that defaults to this behavior. It is usually a deliberate policy implemented with a hook, visitor, or custom decoder, such as Python’s object_pairs_hook or a Go UnmarshalJSON method.
-
Hard error. The parser rejects the document, often in a strict mode or through schema validation. A parser that is last-value-wins by default may still have a strict switch, so “the parser” is not one behavior; its configuration matters.
-
Duplicate-preserving representation. The receiver keeps repeated pairs instead of collapsing them into a map. System.Text.Json JsonDocument preserves the raw duplicate structure on re-serialisation. PostgreSQL json stores an exact input copy and keeps all duplicate key/value pairs, although processing functions treat the last value as operative. Preservation is not the same as resolution: a later query can still choose a winner.
A survey of parser behavior can show a dominant default, but such surveys are corpus-specific. The practical lesson is to test the exact parser, version, and configuration in your pipeline, not to assume a universal default.
Language examples: JavaScript, Python, Go, Java
Use the same input everywhere: {"a":1,"a":2}. The question is not whether the text is valid; it is what each API returns and whether it can report the duplicate.
JavaScript
const input = '{"a":1,"a":2}';
const value = JSON.parse(input);
console.log(value);
console.log(JSON.stringify(value));
Expected output:
{ a: 2 }
{"a":2}
JSON.parse builds an object, so the second a overwrites the first. A reviver runs after that collapse and cannot recover the duplicate. To detect duplicates in JavaScript, use a tokenizing or streaming parser before object construction, or run a duplicate-key linter in CI. JSON.parse alone is not a duplicate detector.
Python
import json
value = json.loads('{"a":1,"a":2}')
print(value)
print(json.dumps(value))
Expected output:
{'a': 2}
{"a": 2}
Python’s default is last-value-wins. object_pairs_hook receives the decoded pairs before they become a dict, so it can reject or choose first/last:
import json
def reject_dupes(pairs):
seen = set()
obj = {}
for key, value in pairs:
if key in seen:
raise ValueError(f"duplicate key: {key!r}")
seen.add(key)
obj[key] = value
return obj
json.loads('{"a":1,"a":2}', object_pairs_hook=reject_dupes)
# ValueError: duplicate key: 'a'
def first_wins(pairs):
obj = {}
for key, value in pairs:
obj.setdefault(key, value)
return obj
print(json.loads('{"a":1,"a":2}', object_pairs_hook=first_wins))
# {'a': 1}
Because the hook sees decoded keys, {"a":1,"\u0061":2} also triggers the duplicate check.
Go
encoding/json into a map or struct is last-value-wins by default:
package main
import (
"encoding/json"
"fmt"
)
func main() {
var m map[string]int
err := json.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
fmt.Printf("%v %v\n", m, err)
}
Expected output:
map[a:2] <nil>
To reject duplicates, decode with a token loop and track keys:
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type Strict struct {
A int
}
func (s *Strict) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
tok, err := dec.Token()
if err != nil {
return err
}
if d, ok := tok.(json.Delim); !ok || d != '{' {
return fmt.Errorf("expected object")
}
seen := map[string]bool{}
for dec.More() {
tok, err := dec.Token()
if err != nil {
return err
}
key := tok.(string)
if seen[key] {
return fmt.Errorf("duplicate key %q", key)
}
seen[key] = true
var raw json.RawMessage
if err := dec.Decode(&raw); err != nil {
return err
}
if key == "a" {
if err := json.Unmarshal(raw, &s.A); err != nil {
return err
}
}
}
_, err = dec.Token()
return err
}
func main() {
var s Strict
err := json.Unmarshal([]byte(`{"a":1,"a":2}`), &s)
fmt.Printf("%+v %v\n", s, err)
}
Expected output:
{A:1} duplicate key "a"
Java
Jackson’s ObjectMapper is last-value-wins by default:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Demo {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree("{\"a\":1,\"a\":2}");
System.out.println(node);
}
}
Expected output:
{"a":2}
Gson also appeared in the last-value-wins group. If your policy requires rejection, do not rely on the default; add an explicit duplicate check in the ingestion path or use a version-specific strict setting after verifying it in your environment. The default result above is the behavior to test first.
Rust and serde_json verification
Rust is a deliberate verification checkpoint in this guide. The evidence used for the other examples does not establish a default for serde_json, so no output is asserted here. Duplicate handling is a property of the parser and the code path, so Rust needs its own test rather than an inference from JavaScript or Go.
The harness has three parts. First, test a generic value:
let v: serde_json::Value = serde_json::from_str(r#"{"a":1,"a":2}"#).unwrap();
println!("{}", v);
Second, test a derived struct:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct S { a: i32 }
let s: S = serde_json::from_str(r#"{"a":1,"a":2}"#).unwrap();
println!("{:?}", s);
Third, use a visitor that rejects duplicates and record the exact error text for your version:
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use std::collections::HashSet;
use std::fmt;
#[derive(Debug)]
struct NoDupes { a: i32 }
impl<'de> Deserialize<'de> for NoDupes {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> Visitor<'de> for V {
type Value = NoDupes;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an object without duplicate keys")
}
fn visit_map<A>(self, mut map: A) -> Result<NoDupes, A::Error>
where
A: MapAccess<'de>,
{
let mut seen = HashSet::new();
let mut a = None;
while let Some(key) = map.next_key::<String>()? {
if !seen.insert(key.clone()) {
return Err(de::Error::custom(format!("duplicate key {key}")));
}
match key.as_str() {
"a" => a = Some(map.next_value()?),
_ => {
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
Ok(NoDupes {
a: a.ok_or_else(|| de::Error::missing_field("a"))?,
})
}
}
deserializer.deserialize_map(V)
}
}
The visitor compares decoded field names, so an escaped Unicode spelling that resolves to the same name is caught. If your policy is first-value-wins, adapt the visitor to ignore later values; do not assume that a library default matches your policy. For a production check, pin the serde_json version and record the observed outputs for both cases.
Duplicate-preserving representations and pipeline risk
Not every receiver collapses duplicates. System.Text.Json JsonDocument preserves the raw duplicate structure on re-serialisation, unlike a dictionary-shaped parser that keeps only the last value. PostgreSQL draws a sharper line between storage and query behavior: json stores an exact input copy and keeps all duplicate key/value pairs, while processing functions treat the last value as operative. jsonb does not keep duplicate object keys; if duplicates are input, only the last value is kept.
A compact PostgreSQL check:
SELECT '{"a":1,"a":2}'::json; -- preserves both members
SELECT '{"a":1,"a":2}'::json->>'a'; -- 2, because processing uses the last value
SELECT '{"a":1,"a":2}'::jsonb; -- {"a": 2}, duplicate removed
The pipeline risk is easy to miss. Service A may preserve the original text or a duplicate-aware document. Service B may parse into a map and keep the last value. Service C may validate against a schema that rejects duplicates. The same payload can therefore be accepted at one hop, normalized at the next, and rejected later. If A re-serialises a JsonDocument, the duplicates may survive; if A materialises a dictionary, they do not. If A stores PostgreSQL json and B reads through jsonb, the operational value changes from a preserved pair to the last value.
Treat duplicate handling as part of the interface contract. Either reject duplicates at ingestion, or document a single canonicalization policy and apply it at every boundary. Log both occurrences when you reject. Do not let one service’s preservation accidentally become another service’s silent overwrite.
Detection and escentmachine positioning
Detection should happen before a parser has a chance to overwrite. Linters are the cheapest first line. The ESLint JSON plugin’s no-duplicate-keys rule warns when two keys have the same decoded text, including Unicode escape sequences that resolve to the same character. JSONLint also warns that duplicate keys are ambiguous and that most parsers silently keep the last occurrence. Those warnings are useful, but they do not cover every runtime path; add a parser-level check where the data enters your system.
Compare decoded keys, not raw bytes. These two documents are different text but the same object name after decoding:
{"a":1,"a":2}
{"a":1,"\u0061":2}
A detector that compares raw key strings will miss the second case. Python’s object_pairs_hook and Go’s decoder token loop see decoded keys, so they catch it. JavaScript’s JSON.parse does not, because it collapses the object before a reviver can inspect it; use a tokenizing parser or a linter for that case.
escentmachine’s JSON Inspector is designed to report duplicates as ambiguity, not as a winner. For each duplicate, show the object path, both occurrences, and both values. For {"a":1,"a":2}, the report should show two members at $.a with values 1 and 2. For {"a":1,"\u0061":2}, it should show the same decoded name and flag the duplicate. For nested input such as {"o":{"a":1,"a":2}}, the path is $.o.a with two occurrences. The Inspector should not silently overwrite the first value with the last, and it should not guess which one the user meant. Where a downstream operation requires a single value, the reject-never-overwrite policy applies: stop with an explainable diagnostic rather than choosing.
That is the practical product position: inspect data, find the mismatch. Run the sample inputs above in the JSON Inspector, confirm that the duplicate diagnostic names the path and both values, and use the same inputs in CI with a duplicate-aware linter or parser. The expected result is not a prettier object with one a; it is a clear report that the object contains two a members and that the receiver’s choice is not yours to make silently. Processing stays local.