Data Formats (JSON, YAML, TOML, XML, CSV) Module 2 – JSON in depth
JSON interop: duplicates, key order, I-JSON
Detect duplicate names that parsers accept silently, see why JavaScript reorders keys, keep parsed JSON from polluting prototypes, and apply the I-JSON rules.
What you will learn
- Detect duplicate member names that standard parsers silently accept
- Explain why JavaScript reorders integer-like keys and when member order matters
- Prevent prototype pollution when merging parsed JSON in JavaScript
- Apply the I-JSON rules (RFC 7493) to make messages safe for every receiver
Before you start
What you’ll learn · Before you start
What you will learn
- Detect duplicate member names that standard parsers silently accept
- Explain why JavaScript reorders integer-like keys and when member order matters
- Prevent prototype pollution when merging parsed JSON in JavaScript
- Apply the I-JSON rules (RFC 7493) to make messages safe for every receiver
Before you start
On this page
{"status": "paid", "status": "refunded"} is valid JSON, and both JSON.parse and Python’s json.loads read it as
refunded without a word. A check that reads the first value sees a paid order, the service that reads the last one
processes a refund, and nothing in either program fails.
You will detect duplicate names that standard parsers accept, with the JSON Pointer of each, see why JavaScript changes the order of some keys and when order matters, keep a parsed document from changing every object of a JavaScript program through its prototype, and apply the I-JSON rules of RFC 7493, which make a message read the same way by every receiver.
Duplicate names: both parsers keep the last value, without a word
RFC 8259 says that the names within an object SHOULD be unique, and calls what receivers do with duplicates “unpredictable”. It also says what parsers do: “Many implementations report the last name/value pair only”; others report an error, and some report every pair. Here is an order with three repeated names, one of them hidden behind an escape:
Python · duplicates/dup_detect.py
"""Find duplicate member names, with the JSON Pointer of the object that holds them."""
import json
class Pairs(list):
"""An object kept as its list of (name, value) pairs, so repeated names stay visible."""
def pointer(path):
"""An RFC 6901 JSON Pointer: each token with ~ written ~0 and / written ~1."""
return "".join("/" + str(t).replace("~", "~0").replace("/", "~1") for t in path)
def duplicates(value, path=()):
"""Every repeated name, as (pointer of its object, name)."""
found = []
if isinstance(value, Pairs):
seen = set()
for name, item in value:
if name in seen:
found.append((pointer(path) or "(root)", name))
seen.add(name)
found += duplicates(item, path + (name,))
elif isinstance(value, list):
for i, item in enumerate(value):
found += duplicates(item, path + (i,))
return found
text = '{"id": "BK-1", "status": "paid", "items": [{"sku": "A", "qty": 1, "qty": 5}], "status": "refunded", "a/b": {"x": 1, "\\u0078": 2}}'
print("json.loads keeps:", json.loads(text)["status"], json.loads(text)["items"][0]["qty"])
for where, name in sorted(duplicates(json.loads(text, object_pairs_hook=Pairs)), key=lambda d: d[1]):
print(f"duplicate {name!r} in {where}") Output
json.loads keeps: refunded 5 duplicate 'qty' in /items/0 duplicate 'status' in (root) duplicate 'x' in /a~1b
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dup_detect.py
JavaScript · duplicates/dup_detect.mjs
// JSON.parse keeps the last of two equal names and offers no hook to notice, so this scanner reads the text itself
// and reports each repeated name with the JSON Pointer of its object. Names are compared after unescaping.
function duplicateNames(text) {
const found = [];
let i = 0;
const skipSpace = () => { while (' \t\n\r'.includes(text[i]) && i < text.length) i++; };
const readString = () => { // returns the unescaped string; JSON.parse does the unescaping
const start = i++;
while (text[i] !== '"') i += text[i] === '\\' ? 2 : 1;
i++;
return JSON.parse(text.slice(start, i));
};
const pointer = (path) => path.map((t) => '/' + String(t).replaceAll('~', '~0').replaceAll('/', '~1')).join('') || '(root)';
const readValue = (path) => {
skipSpace();
if (text[i] === '{') {
i++;
const seen = new Set();
for (skipSpace(); text[i] !== '}'; skipSpace()) {
if (text[i] === ',') { i++; skipSpace(); }
const name = readString();
if (seen.has(name)) found.push(`duplicate ${JSON.stringify(name)} in ${pointer(path)}`);
seen.add(name);
skipSpace(); i++; // the colon
readValue([...path, name]);
}
i++;
} else if (text[i] === '[') {
i++;
for (let n = 0; (skipSpace(), text[i] !== ']'); n++) {
if (text[i] === ',') i++;
readValue([...path, n]);
skipSpace();
}
i++;
} else if (text[i] === '"') readString();
else while (i < text.length && !',]} \t\n\r'.includes(text[i])) i++; // a number, true, false or null
};
JSON.parse(text); // refuse invalid JSON first, so the scanner only ever sees valid text
readValue([]);
return found.sort();
}
const text = '{"id": "BK-1", "status": "paid", "items": [{"sku": "A", "qty": 1, "qty": 5}], "status": "refunded", "a/b": {"x": 1, "\\u0078": 2}}';
console.log('JSON.parse keeps:', JSON.parse(text).status, JSON.parse(text).items[0].qty);
for (const line of duplicateNames(text)) console.log(line); Output
JSON.parse keeps: refunded 5 duplicate "qty" in /items/0 duplicate "status" in (root) duplicate "x" in /a~1b
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node dup_detect.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Both parsers kept refunded and 5, the last values, and neither said anything; Python’s documentation states that
it “ignores all but the last name-value pair”, and ECMAScript’s says that earlier values for the same key “shall be
overwritten”. The detectors found what the parsers hid. Python’s object_pairs_hook hands over each object as its
list of pairs, before any name is dropped; JavaScript has no such hook, so the second program scans the text itself,
after JSON.parse has confirmed that the text is valid. Both compare names after unescaping, which is why "x" and
"\u0078" count as the same name, and both report the place as a JSON Pointer: A string that names one value inside a JSON document, such as /items/0/qty, defined by RFC 6901. Inside a name, ~ is written ~0 and / is written ~1.: /a~1b
is the member a/b, with / written ~1, as RFC 6901 asks.
The danger is two readers that resolve a duplicate name: A name that appears more than once in the same JSON object. The grammar allows it, RFC 8259 says names SHOULD be unique, and parsers differ in which value they keep. differently. Step through one request read by a check that stops at the first value and by a service that keeps the last:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The request. The client sends an order whose status appears twice, first "paid", then "refunded". The text is valid JSON: the grammar allows a name to repeat.
- The edge allows it. The check at the edge looks for the first "status" in the text, finds "paid", and lets the request through. A pattern or a streaming reader that stops at the first match behaves like this.
- The service reads another value. The service parses the same bytes with JSON.parse, which keeps the last value of a repeated name, as the ECMAScript specification says. For it the status is "refunded".
- The readers disagree. One request now means two different orders. RFC 8259 calls the behaviour with duplicate names unpredictable, and here the check allowed what the service never processed.
- Refused at the edge instead. With a duplicate check first, the edge finds "status" twice in the root object and refuses the request with a clear error, before any reader takes a value.
- One meaning per message. Refusing duplicates, as I-JSON requires, gives every reader the same single value for every name, so a check and a service can no longer read two different messages.
Key idea
A repeated name is valid JSON with no single meaning, so refuse it at the first program that reads the message, before any reader picks a value.
Checkpoint
Check: Duplicate names
Member order: JavaScript lists integer-like keys first
In JSON’s data model an object is an unordered collection of members, and RFC 8259 notes that implementations whose behaviour does not depend on member order will be interoperable. Parsers still hand members over in some order, and the two languages differ:
JavaScript · order/key_order.mjs
// The order of members after JSON.parse: integer-like names first, in numeric order, then the rest as written.
const text = '{"b": 1, "2": 2, "1": 3, "a": 4, "10": 5}';
const obj = JSON.parse(text);
console.log('Object.keys: ', Object.keys(obj).join(', '));
console.log('round trip: ', JSON.stringify(obj));
// When the order is data, send it as an array: arrays keep their order in every parser.
const ordered = new Map(JSON.parse('[["b", 1], ["2", 2], ["1", 3], ["a", 4], ["10", 5]]'));
console.log('array of pairs:', [...ordered.keys()].join(', ')); Output
Object.keys: 1, 2, 10, b, a
round trip: {"1":3,"2":2,"10":5,"b":1,"a":4}
array of pairs: b, 2, 1, a, 10
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node key_order.mjs
Python · order/key_order.py
"""The same document in Python: a dict keeps the order in which the names appear in the text."""
import json
text = '{"b": 1, "2": 2, "1": 3, "a": 4, "10": 5}'
obj = json.loads(text)
print("keys: ", ", ".join(obj))
print("round trip: ", json.dumps(obj, separators=(",", ":"))) Output
keys: b, 2, 1, a, 10
round trip: {"b":1,"2":2,"1":3,"a":4,"10":5}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 key_order.py
Runs on this device, in your browser. The first run of each language downloads it: JavaScript (about 0.6 MB) or Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Python kept the order of the text, b, 2, 1, a, 10, because a dict remembers the order of insertion. JavaScript
listed 1, 2, 10 first, in numeric order, and then b and a as written, and JSON.stringify wrote that new order
back. The rule is in the ECMAScript specification: an object’s own keys come out with every array index first, in
ascending numeric order, then the other strings in the order they were created. So names such as "2" and "10"
move, and "b" and "a" do not.
Order matters in three places: when bytes are compared or signed, as in a hash or a signature over the text; when people read diffs; and when the order is itself data, such as the ranking of search results. For the first, canonical JSON (RFC 8785, a later lesson of this module) defines one order; for the last, send an array, which keeps its order in every parser, as the last line of the JavaScript output shows.
Relying on member order
Wrong: taking “the first key of the object” as the most important result, or comparing two JSON texts as strings
after a round trip through JSON.parse. Right: put ranked items in an array, and compare parsed values, or
canonical forms, never re-serialised text.
Checkpoint
Check: Member order in JavaScript and Python
A prototype key is harmless in JSON.parse and dangerous in a merge
Every ordinary JavaScript object inherits from Object.prototype, and the name __proto__ has a setter there that
changes an object’s prototype. JSON.parse does not run that setter: during parsing a "__proto__" member is created
as an ordinary own property, as the specification requires. The danger starts when code copies or merges the parsed
document. This program takes a settings document with a "__proto__" member and merges it four ways, then checks
whether a brand-new empty object has gained a property:
// A settings document with a "__proto__" member, merged into defaults four ways. The check after each merge asks
// whether a brand-new, empty object has gained a property: if it has, Object.prototype itself was changed.
const body = '{"theme": "dark", "__proto__": {"isAdmin": true}}';
const parsed = JSON.parse(body);
const check = (label) => {
console.log(`${label.padEnd(18)} ({}).isAdmin is ${({}).isAdmin}`);
delete Object.prototype.isAdmin; // undo any change, so each merge starts clean
};
console.log('JSON.parse made own keys:', Object.keys(parsed).join(', '));
check('after JSON.parse');
const assigned = Object.assign({}, parsed); // [[Set]] of "__proto__" runs the setter: the copy's prototype changes
console.log('Object.assign copy: isAdmin', assigned.isAdmin, '| own keys:', Object.keys(assigned).join(', '));
check('after assign');
function naiveMerge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) naiveMerge((target[key] ??= {}), source[key]);
else target[key] = source[key];
}
return target;
}
naiveMerge({ theme: 'light' }, parsed); // target["__proto__"] is Object.prototype itself
check('after naive merge');
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue; // never merge these names
const value = source[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const inner = Object.hasOwn(target, key) ? target[key] : Object.create(null);
target[key] = safeMerge(inner, value);
} else target[key] = value;
}
return target;
}
const settings = safeMerge(Object.assign(Object.create(null), { theme: 'light' }), parsed);
console.log('safe merge result:', JSON.stringify(settings));
check('after safe merge'); Output
JSON.parse made own keys: theme, __proto__
after JSON.parse ({}).isAdmin is undefined
Object.assign copy: isAdmin true | own keys: theme
after assign ({}).isAdmin is undefined
after naive merge ({}).isAdmin is true
safe merge result: {"theme":"dark"}
after safe merge ({}).isAdmin is undefined
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node safe_merge.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Take one defence away
Press Edit, delete the line of safeMerge that skips __proto__, constructor and prototype, and run it again.
The last check still prints undefined, because the merged objects have no prototype to change, but the result now
carries "__proto__" as data, which the next careless copy could act on: keep both defences.
JSON.parse kept __proto__ as an own key, and nothing else changed. Object.assign copies with ordinary
assignment, which runs the setter, so the copy’s prototype became {"isAdmin": true}: the copy reports isAdmin
without having it as its own key. The naive deep merge is the serious case. It reached target["__proto__"], which is
Object.prototype itself, and merged isAdmin into it, so every object in the program, including an empty one, now
reports isAdmin: true. This weakness is called prototype pollution: A weakness of JavaScript programs in which input data adds or changes properties of a shared prototype such as Object.prototype, so every object seems to have them..
The safe merge stops all of it with three habits, the second of which OWASP’s prevention cheat sheet recommends:
| Defence | How the safe merge applies it |
|---|---|
| Skip dangerous names | It never copies __proto__, constructor or prototype |
| Use objects without a prototype | It builds its result with Object.create(null), so no inherited setter exists |
| Copy own keys only | It walks Object.keys(source), never for...in, which also visits inherited keys |
A Map is a fourth way: its keys are data, never properties, so a key named __proto__ is just a string. And a schema
check of the parsed settings, allowing only the names a settings object can have, refuses the document before any merge.
Test the fix with the check that found the bug
Keep the check from the program in your tests: merge a document with a "__proto__" member, then assert that
({}).isAdmin is still undefined. It fails for the naive merge and passes for the safe one.
Checkpoint
Check: Prototype pollution through merges
I-JSON: the rules that make a message safe for every receiver
RFC 7493, a Proposed Standard, defines I-JSON: The Internet JSON profile of RFC 7493: JSON in UTF-8, without duplicate names or lone surrogates, with numbers within binary64 and big integers sent as strings., the Internet JSON profile: JSON with the choices made that RFC 8259 leaves open, so that every conforming receiver reads a message the same way. Its rules, with the lessons of this module that show why each one exists:
| Rule | RFC 7493 | Why |
|---|---|---|
| UTF-8 only | MUST, 2.1 | Readers that assume UTF-8 misread anything else, as the encodings lesson showed |
| No surrogates or noncharacters in names and strings, raw or escaped | MUST NOT, 2.1 | A lone surrogate has no UTF-8 form |
| Numbers within binary64’s magnitude and precision; big integers as strings | SHOULD NOT exceed, RECOMMENDED strings, 2.2 | Readers above 2^53 − 1 round, as the numbers lesson showed |
| No duplicate names, compared after unescaping | MUST NOT, 2.3 | Readers resolve duplicates differently |
| An object or an array at the top level | SHOULD, 4.1 | Older software expects one |
| Ignore members whose names are not recognised | Recommended policy, 4.2 | Lets a protocol add members without breaking old receivers |
| Times as RFC 3339 strings, binary data as base64url | RECOMMENDED, 4.3 and 4.4 | JSON has no date or binary type |
The rules that a program can check mechanically fit in one function. This one checks six messages:
"""Six messages checked against the I-JSON rules of RFC 7493 that a program can test."""
import json
MAX_EXACT = 2**53 - 1 # 9007199254740991, the largest integer every binary64 receiver holds exactly
def bad_text(s):
"""A surrogate (a lone half) or a noncharacter, which I-JSON strings must not contain."""
for c in s:
cp = ord(c)
if 0xD800 <= cp <= 0xDFFF:
return f"lone surrogate U+{cp:04X}"
if 0xFDD0 <= cp <= 0xFDEF or cp & 0xFFFE == 0xFFFE:
return f"noncharacter U+{cp:04X}"
return None
def problems(data):
"""The I-JSON rules a message breaks, in the order they are checked."""
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
return ["not UTF-8"]
found = []
def pairs(items):
names = [name for name, _ in items]
found.extend(f"duplicate name {n!r}" for n in sorted({n for n in names if names.count(n) > 1}))
found.extend(p for p in map(bad_text, names) if p)
return dict(items)
value = json.loads(text, object_pairs_hook=pairs)
stack = [value]
while stack:
v = stack.pop()
if isinstance(v, dict):
stack.extend(v.values())
elif isinstance(v, list):
stack.extend(v)
elif isinstance(v, bool):
continue
elif isinstance(v, int) and abs(v) > MAX_EXACT:
found.append(f"integer {v} beyond 2^53 - 1")
elif isinstance(v, float) and abs(v) == float("inf"):
found.append("number beyond binary64")
elif isinstance(v, str) and bad_text(v):
found.append(bad_text(v))
if not isinstance(value, (dict, list)):
found.append("top level is not an object or an array (a SHOULD)")
return found
messages = {
"id as a string": b'{"id": "12345678901234567890", "qty": 2}',
"id as a number": b'{"id": 12345678901234567890, "qty": 2}',
"qty twice": b'{"qty": 1, "qty": 2}',
"half a surrogate pair": b'{"name": "\\ud800"}',
"UTF-16 with a BOM": '{"name": "Pune"}'.encode("utf-16"),
"a string on its own": b'"just a string"',
}
for label, data in messages.items():
print(f"{label:<22} {'; '.join(problems(data)) or 'I-JSON'}") Output
id as a string I-JSON id as a number integer 12345678901234567890 beyond 2^53 - 1 qty twice duplicate name 'qty' half a surrogate pair lone surrogate U+D800 UTF-16 with a BOM not UTF-8 a string on its own top level is not an object or an array (a SHOULD)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ijson_check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Only the first message passes, the one with its 20-digit id as a string. The second carries the same id as a number,
beyond 2^53 − 1; the third repeats qty; the fourth holds half a surrogate pair; the fifth is UTF-16; the sixth is a
bare string at the top level, which I-JSON allows but advises against. Running such a check where messages enter a
system, and refusing what fails, is a cheap way to make every program behind it agree.
Checkpoint
Check: The I-JSON rules
Practise: an I-JSON checker with pointers
The exercise combines the duplicate finder and the I-JSON check: it reports every duplicate name, big integer and lone
surrogate of a message with the JSON Pointer of the member it is about, including names that collide only after
unescaping and pointers whose names hold ~ or /:
Exercise · Medium · Python
An I-JSON checker that names every problem
Write check_i_json(data) in ijson.py. It takes the bytes of a JSON message and returns the list of I-JSON problems it finds, sorted, each with the JSON Pointer (RFC 6901) of the member or value it is about:
"not UTF-8"alone, when the bytes are not UTF-8;"duplicate /qty"for every name that repeats in an object, at the pointer of that member;"big integer /id"for every integer greater than 2^53 − 1 or less than −(2^53 − 1);"lone surrogate /name"for every string value that holds half of a surrogate pair.
Names are compared after unescaping, so "a" and "\u0061" are the same name. A pointer writes ~ as ~0 and / as ~1 inside a name, so a repeated m~n inside a/b is /a~1b/m~0n. A clean message gives []. true and false are not integers, even though Python's bool is a kind of int.
Starter code · ijson.py
import json
class Pairs(list):
"""An object kept as its list of (name, value) pairs."""
def check_i_json(data):
"""The I-JSON problems of a message, sorted, each with its JSON Pointer."""
problems = []
return sorted(problems) The sample tests · test_ijson.py
from ijson import check_i_json
def test_clean_message():
"""finds nothing in a message that follows the rules"""
assert check_i_json(b'{"id": "12345678901234567890", "qty": 2, "flag": true, "big": 9007199254740991}') == []
def test_duplicates():
"""reports repeated names at the pointer of the member, nested ones too"""
assert check_i_json(b'{"qty": 1, "qty": 2}') == ["duplicate /qty"]
assert check_i_json(b'{"items": [{"sku": "A", "sku": "B"}]}') == ["duplicate /items/0/sku"]
def test_names_compared_after_unescaping():
"""treats a name and its escaped form as the same name"""
assert check_i_json(b'{"a": 1, "\\u0061": 2}') == ["duplicate /a"]
def test_pointer_escapes():
"""writes ~ as ~0 and / as ~1 in pointers"""
assert check_i_json(b'{"a/b": {"m~n": 1, "m~n": 2}}') == ["duplicate /a~1b/m~0n"]
def test_big_integers():
"""reports integers beyond 2^53 - 1 in either direction"""
assert check_i_json(b'{"id": 9007199254740993, "n": [-9007199254740992, 7]}') == ["big integer /id", "big integer /n/0"]
def test_lone_surrogates():
"""reports a lone surrogate, but not a surrogate pair"""
assert check_i_json(b'{"name": "\\ud800", "ok": "\\ud83d\\udcda"}') == ["lone surrogate /name"]
def test_not_utf8():
"""stops at bytes that are not UTF-8"""
assert check_i_json(b'{"a": "\xff"}') == ["not UTF-8"] A hint
Parse with json.loads(text, object_pairs_hook=Pairs), where class Pairs(list) keeps each object as its list of (name, value) pairs, as the lesson's duplicate finder does. Then walk the result with the path so far, building the pointer as you go, and check each kind of problem where you meet it. Test isinstance(v, bool) before int.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then take the questions on duplicates, member order, merges and I-JSON. Each one shows why an answer holds, and a slip sends you back to that idea first:
Practice that adapts to you
Practise this lesson
12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): what does a JSON parser do with an object that repeats a name?
The grammar allows it, so the text is valid JSON, but RFC 8259 says names SHOULD be unique and calls the behaviour of
receivers unpredictable. In practice most parsers keep the last value: JSON.parse does by the ECMAScript
specification, and Python’s json.loads does by its documentation, so {"qty": 1, "qty": 2} gives 2 in both, without
a warning. Other parsers keep the first value, refuse the text or report every pair, which is why a check and a service
can read different values from one message. To detect duplicates in Python, use object_pairs_hook, which sees every
pair; in JavaScript, scan the text. I-JSON, RFC 7493, forbids duplicates, compared after unescaping.
Two harder questions follow, about a check and a service that read different values and about a settings merge that changed every user. Decide what you would check first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your gateway checks that a refund request's status is allowed and then passes the original bytes to an order service. A request with "status" twice was allowed by the gateway and processed by the service with the other value. Explain how, and how you would close the gap and prove it closed.
-
Mid to senior
A Node.js settings endpoint deep-merges each user's JSON into the default settings. After one user's request, every settings object in the process reports isAdmin: true. Explain what happened, fix the merge, and describe the test that keeps it fixed.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What do JSON.parse and json.loads return for {"qty": 1, "qty": 2}?
Both keep the last value, 2, without a warning. Other parsers keep the first, refuse the text or report both, so two readers can disagree.
-
How do you find repeated names in Python before a dict hides them?
Pass object_pairs_hook to json.loads: it receives every pair of each object, repeats included. Compare names after unescaping.
-
In which order does JavaScript list the keys of a parsed object?
Integer-like keys first, in numeric order, then the other keys in insertion order. Keep meaning out of member order: use an array when order matters.
-
When does a "__proto__" key in JSON become dangerous?
Not in JSON.parse, which keeps it as an ordinary own property, but in Object.assign or a naive deep merge, which can change an object's prototype.
-
What makes a merge of parsed JSON safe from prototype pollution?
Skip __proto__, constructor and prototype; build results with Object.create(null); copy own keys only, with Object.keys rather than for...in; or use a Map.
-
What does I-JSON (RFC 7493) ask of a message?
UTF-8; no lone surrogates or noncharacters; no repeated names, compared after unescaping; numbers within binary64, with big integers as strings; and preferably an object or an array at the top.
Key takeaways
- A repeated name is valid JSON;
JSON.parseandjson.loadskeep the last value silently, so detect duplicates withobject_pairs_hookor a scanner, compare names after unescaping, and refuse them at the edge. - JSON objects are unordered, but JavaScript lists integer-like keys first in numeric order, so never let meaning depend on member order: use arrays for ordered data and canonical forms for bytes that are compared.
JSON.parsekeeps"__proto__"as an own property, butObject.assignand naive deep merges can change prototypes; skip dangerous names, build objects withObject.create(null), copy own keys only, or use aMap.- I-JSON requires UTF-8, no lone surrogates and no duplicate names, keeps numbers within binary64 with big integers as strings, and advises an object or an array at the top; check those rules where messages enter.
Terms in this lesson
- duplicate name
- A name that appears more than once in the same JSON object. The grammar allows it, RFC 8259 says names SHOULD be unique, and parsers differ in which value they keep.
- I-JSON
- The Internet JSON profile of RFC 7493: JSON in UTF-8, without duplicate names or lone surrogates, with numbers within binary64 and big integers sent as strings.
- JSON Pointer
- A string that names one value inside a JSON document, such as /items/0/qty, defined by RFC 6901. Inside a name, ~ is written ~0 and / is written ~1.
- prototype pollution
- A weakness of JavaScript programs in which input data adds or changes properties of a shared prototype such as Object.prototype, so every object seems to have them.
References
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format, section 4 (IETF)
- RFC 7493: The I-JSON Message Format (IETF)
- RFC 6901: JavaScript Object Notation (JSON) Pointer (IETF)
- ECMAScript 2026 Language Specification: JSON.parse, OrdinaryOwnPropertyKeys, PropertyDefinitionEvaluation (Ecma International)
- json: JSON encoder and decoder (Python 3.14), repeated names and object_pairs_hook (Python Software Foundation)
- Prototype Pollution Prevention Cheat Sheet (OWASP)
- Object.assign() (Mozilla)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress