ZIV LABS
Engineering notes

JSON validation pitfalls

Valid JSON is not the same as safe JSON. A document can pass a parser and still mean different things to the services that read it. This note covers five cases where that gap shows up in practice. Every result below was produced by running the commands shown, on the versions in the byline. Other versions may differ.

1. Duplicate keys: the last one wins, silently

RFC 8259 says object member names SHOULD be unique and that the behavior of software receiving duplicates is unpredictable. It does not call them an error, so parsers accept them.

$ node -e 'console.log(JSON.parse(`{"role":"user","role":"admin"}`))'
{ role: 'admin' }
$ python3 -c 'import json; print(json.loads("{\"role\":\"user\",\"role\":\"admin\"}"))'
{'role': 'admin'}

Both keep the last value. Not every parser does: some keep the first, and some reject duplicates. If a gateway checks the first role and the backend reads the last, the two disagree about the same bytes. When a document crosses a trust boundary, reject duplicates. Python can do this with object_pairs_hook:

import json

def no_duplicates(pairs):
    keys = [k for k, _ in pairs]
    if len(keys) != len(set(keys)):
        raise ValueError("duplicate key")
    return dict(pairs)

json.loads('{"a":1,"a":2}', object_pairs_hook=no_duplicates)
# ValueError: duplicate key

JSON.parse has no equivalent option. In JavaScript you need a strict parser library, or a validator that reports duplicates before parsing.

2. Large integers lose precision in JavaScript

JSON numbers have no size limit in the grammar. JavaScript parses every number as a 64-bit float, which holds integers exactly only up to 253−1 (9007199254740991).

$ node -e 'console.log(JSON.parse(`{"id":9007199254740993}`).id)'
9007199254740992
$ python3 -c 'import json; print(json.loads("{\"id\":9007199254740993}"))'
{'id': 9007199254740993}

The ID changed by one in JavaScript and no error was raised. Python kept it. Any pipeline that parses and re-serializes in JavaScript can corrupt 64-bit IDs. The usual fixes: send IDs as strings (the most robust option), or parse with a library that returns BigInt for unsafe integers. Check with Number.isSafeInteger() after parsing if you cannot change the producer.

3. A byte order mark breaks the parse

RFC 8259 §8.1 says implementations must not add a BOM to JSON text, and parsers may ignore one. Many editors on Windows add it anyway.

$ node -e 'JSON.parse("\uFEFF{\"a\":1}")'
SyntaxError: Unexpected token '', "{"a":1}" is not valid JSON
$ python3 -c 'import json; json.loads("\ufeff{\"a\":1}")'
json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)

The BOM is invisible in most editors, so the error message looks wrong: the first character appears to be a normal {. If a file "looks valid" but fails at line 1, column 1, check the first bytes with head -c 3 file | xxd. In Python, open the file with encoding="utf-8-sig".

4. NaN and Infinity: accepted by one, rejected by the other

NaN and Infinity are not valid JSON. Python's parser accepts them by default; JavaScript's does not.

$ python3 -c 'import json; print(json.loads("{\"a\":NaN}"))'
{'a': nan}
$ node -e 'JSON.parse(`{"a":NaN}`)'
SyntaxError: Unexpected token 'N', "{"a":NaN}" is not valid JSON

A Python service can therefore emit a payload that no JavaScript client can read. Pass allow_nan=False to json.dumps so the producer fails first. A related case is overflow: both parsers turn 1e999 into infinity (Infinity in JavaScript, inf in Python) rather than failing, so a value that was valid text becomes a non-JSON value after parsing.

5. Things that look like JSON but are not

The strict grammar excludes trailing commas, single quotes, unquoted keys, and comments. All of these fail in JSON.parse:

{"a":1,}        → Expected double-quoted property name in JSON at position 7
{'a':1}         → Expected property name or '}' in JSON at position 1
{a:1}           → Expected property name or '}' in JSON at position 1
{"a":1 /* c */} → Expected ',' or '}' after property value in JSON at position 7

These are typical of hand-edited config files (JSON5 or JSONC dialects). Error wording differs between Node versions and between engines, so do not match on message text; use the reported position only to help a human find the problem.

One more check: __proto__ is not an attack by itself

A common worry is that {"__proto__":{"admin":true}} pollutes prototypes during parsing. In Node v24.7.0, JSON.parse creates an ordinary own property named __proto__ and does not change the prototype:

$ node -e 'const o=JSON.parse(`{"__proto__":{"admin":true}}`); console.log(Object.keys(o), o.admin, ({}).admin)'
[ '__proto__' ] undefined undefined

The risk appears later, when application code merges parsed objects with a naive recursive merge or assigns keys dynamically. Validate the parsed shape and avoid unsafe merges.

Practical checklist

  • Reject duplicate keys at trust boundaries.
  • Send 64-bit IDs and money amounts as strings, or document the numeric range.
  • Strip or handle a BOM when reading files; never add one when writing.
  • Use allow_nan=False (Python) on the producer side.
  • Validate shape and types after parsing, not only syntax.

What we did not verify

We tested only Node.js v24.7.0 and Python 3.13.3. We did not test other languages' parsers, so statements such as "some parsers keep the first duplicate" are general observations from the RFC's wording, not results from this test.

References

Related tools

  • JSON Validator — Checks syntax in your browser. Test it with the duplicate-key and BOM samples above to see how it behaves.
  • JSON Formatter — Parses with JavaScript's JSON.parse, so the duplicate-key and large-number behavior described above applies.

Small Tools is built and operated by ZIV LABS. These links go to tools.ziv-labs.com.

Related notes

  • JWT verification mistakes — Four tokens a flawed JWT verifier accepts and a strict one rejects: alg none, RS256-to-HS256 confusion, and unchecked claims, reproduced with runnable Node.js code.

Further reading