Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Ruby 4.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Find every duplicate name, with its JSON Pointer

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

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:

Interactive · message by message One request, two readers, two different orders Step through one request whose status appears twice. Watch which value each reader takes, and where the fix stops it.
One request, two readers, two different orders, step 6 of 6. Messages so far: Client to Check at the edge: status twice; Check at the edge to Order service: same bytes; Check at the edge to Client: 400: duplicate status.Clientsends the orderCheck at the edgeduplicate status: refusedOrder servicenever reachedstatus twicesame bytes400: duplicate status

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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".
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 What do JSON.parse and Python's json.loads return for the status of {"status": "paid", "status": "refunded"}?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "refunded" in both, without a warning

    RFC 8259 calls the behaviour with duplicates unpredictable and says many implementations report the last pair only. Both of these do, silently, so a duplicate check has to be added on purpose.

  2. Question 2 of 2 Is {"x": 1, "\u0078": 2} an object with a duplicate name?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes; after unescaping, both names are x

    A duplicate check that compares raw text misses this case. RFC 7493 counts names as duplicates when they match after escapes are resolved.

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:

The same five members in JavaScript and in Python

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

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

  1. One question In which order does Object.keys list the members of JSON.parse('{"b": 1, "2": 2, "1": 3, "a": 4, "10": 5}')?

    Choose one answer.

    Show the answer: One question

    Answer: 1, 2, 10, b, a

    key_order.mjs printed 1, 2, 10, b, a, and JSON.stringify wrote the members back in that order. Python's dict kept b, 2, 1, a, 10.

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:

One settings document, merged four ways JavaScript · proto/safe_merge.mjs
// 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

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

  1. Question 1 of 2 After parsed = JSON.parse('{"__proto__": {"isAdmin": true}}'), what does Object.assign({}, parsed) do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It sets the copy's prototype to the parsed object, so the copy reports isAdmin as true

    safe_merge.mjs printed isAdmin true for the copy and an empty object still clean. A deep merge that follows target["__proto__"] goes further and changes Object.prototype.

  2. Question 2 of 2 Which of these stop a deep merge of parsed JSON from changing Object.prototype?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Walking Object.keys(source) instead of for...in
    • Building merged objects with Object.create(null)
    • Skipping the names __proto__, constructor and prototype

    The safe merge in safe_merge.mjs uses all three defences, and the check after it found ({}).isAdmin still undefined. OWASP's prevention cheat sheet recommends objects without a prototype, and Map, for such data.

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 I-JSON Python · ijson/ijson_check.py
"""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

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.

JSON Validator Check a document of your own for syntax errors, duplicate names and numbers that lose precision.

Checkpoint

Check: The I-JSON rules

  1. One question Which of the six messages of ijson_check.py follows the I-JSON rules?

    Choose one answer.

    Show the answer: One question

    Answer: The one with its 20-digit id as a string

    Only the first message passed. The others broke the rules on numbers, duplicate names, surrogates, encoding and the top level, one each.

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.

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.

  1. Question 1 of 12 What does RFC 8259 say about names within an object?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: They SHOULD be unique, and behaviour with duplicates is unpredictable

    The grammar allows duplicates, the RFC discourages them, and implementations differ: last value, first value, an error, or every pair.

  2. Question 2 of 12 What does this print?

    Read the code, then choose one answer.

    import json
    print(json.loads('{"qty": 1, "qty": 2}', object_pairs_hook=list))
    Show the answer: Question 2 of 12

    Answer: [('qty', 1), ('qty', 2)]

    The hook is called with the ordered list of pairs of each object, which is why dup_detect.py could find every repeated name.

  3. Question 3 of 12 A gateway checks request bodies and a service behind it processes them, each with its own parser. Where should duplicate names be refused?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: At the gateway, the first program that reads the message, before any value is used

    The stepped figure showed a check that saw paid and a service that ran refunded. A duplicate check at the edge turned the same request into a clear refusal.

  4. Question 4 of 12 In which order does Python's json.loads keep the members of an object?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: The order in which they appear in the text

    key_order.py printed b, 2, 1, a, 10, the order of the text. The data model still says objects are unordered, so code should not rely on it.

  5. Question 5 of 12 A search service returns results as {"9001": {...}, "42": {...}, "1337": {...}}, best first. What does a JavaScript client see?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: The results in numeric order of their ids, 42, 1337, 9001, so the ranking is lost

    When the order is data, send an array, which keeps its order in every parser, as the array of pairs in key_order.mjs did.

  6. Question 6 of 12 A test compares a service's JSON response with an expected JSON string, character for character, and fails after the service switches from Python to Node.js, although the data is the same. What is the fix?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Parse both texts and compare the values, or compare canonical forms

    JSON.stringify writes integer-like keys first, Python keeps the order of insertion, so the texts differ while the values are equal. Compare what the data means, not how it is spelled.

  7. Question 7 of 12 What does JSON.parse do with a member named "__proto__"?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: It creates an ordinary own property named __proto__, and the prototype is unchanged

    JSON.parse itself is safe. The risk is in what code does next with the parsed object: assignment, Object.assign and merges.

  8. Question 8 of 12 Why did the naive deep merge make ({}).isAdmin true?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: target["__proto__"] read Object.prototype, and the merge wrote isAdmin into it

    Every ordinary object inherits from Object.prototype, so one write there reaches every object of the program. That is why the fix skips the name and uses objects without a prototype.

  9. Question 9 of 12 A service stores per-user preferences whose names users choose. Which structure avoids prototype pollution by design?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: A Map, whose keys are data and never properties

    OWASP's cheat sheet recommends Map and Set, or objects created with Object.create(null), for data whose names come from outside.

  10. Question 10 of 12 Which encoding must an I-JSON message use?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: UTF-8

    The checker refused the UTF-16 message with not UTF-8 before parsing anything.

  11. Question 11 of 12 How does I-JSON say a sender should transmit a 64-bit integer id?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: As a JSON string, which the receiver understands as the integer

    The first message of ijson_check.py carried its id as a string and passed; the second carried it as a number and failed the check.

  12. Question 12 of 12 A protocol adds a new member to its messages. Which I-JSON recommendation keeps old receivers working?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: A Must-Ignore policy, in which members whose names are not recognised are ignored

    I-JSON recommends Must-Ignore to protocol designers, and names a top-level object whose unrecognised members must be ignored as a good way to support it. A schema that allows extra members expresses it, while still checking the members it knows.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.parse and json.loads keep the last value silently, so detect duplicates with object_pairs_hook or 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.parse keeps "__proto__" as an own property, but Object.assign and naive deep merges can change prototypes; skip dangerous names, build objects with Object.create(null), copy own keys only, or use a Map.
  • 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.