Guide
How to test JSON parsers: numbers, Unicode and huge files
Big integers, Unicode, nesting, NDJSON, invalid and empty input, and 100 MB files — the JSON cases that break parsers, with sample files for each.
JSON has a small grammar, so it’s easy to assume every parser handles it the same way. They don’t. Implementations disagree on large numbers, on what counts as valid input, on how deep nesting can go and on how much memory a file is allowed to use. These are the cases worth testing, whether you’re writing a parser, choosing a library or hardening an import endpoint.
1. Every type, and the awkward values
Start with JSON with every data type. It is one small object containing strings with Unicode and escapes, integers, negatives, a float, an exponent (6.02e+23), true, false, null, an empty string, an empty array, an empty object, a mixed array, four levels of nesting and an ISO date string.
The most important value in it is "big_integer": 9007199254740993, which is 2⁵³ + 1. JavaScript numbers are IEEE 754 doubles, so they can’t represent it:
curl -s https://files.samplefiles.com/data/json/sample-types.json \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).big_integer))'
# 9007199254740992 (off by one, silently)This is a real problem for IDs from databases and APIs: Twitter famously added id_str fields for this reason. Your options:
- JavaScript: in Node 21+ and current Chrome,
JSON.parsepasses the reviver a thirdcontextargument whosesourceis the raw text, so you can turn large numbers intoBigInt. Libraries such asjson-bigintdo the same on older runtimes. - Python:
jsonparses integers with arbitrary precision. Floats become doubles unless you passparse_float=decimal.Decimal. - Go: use
json.Decoder.UseNumber()instead of decoding intofloat64.
Also check round-tripping. The array holds 3.0, which Python reads as a float and JavaScript writes back out as 3. If your system signs or hashes JSON, a parse-and-reserialize step changes the bytes.
2. Unicode and escapes
The unicode field holds "Grüße, 世界, مرحبا, 🌍": Latin with diacritics, CJK, right-to-left Arabic and an emoji outside the Basic Multilingual Plane. In JavaScript and Java, that emoji is two UTF-16 code units, so length, substring and truncation can split it in half. The escaped field checks \", \\, \n and \t.
RFC 8259 requires UTF-8 for JSON exchanged between systems. If your parser reads bytes, test what it does with a byte-order mark: Python’s json.loads rejects a decoded string that starts with one and tells you to use utf-8-sig. For heavier string testing, the Unicode stress-test text has ZWJ emoji sequences, combining marks, zero-width characters and homoglyphs that you can embed in JSON fixtures.
3. Nesting and structure
Nested orders has 100 orders, each with a nested customer, address and array of line items. It is the shape most real APIs return, and a good test for code that flattens JSON into tables.
Deep nesting is also a resource problem. Recursive parsers run out of stack: Python raises RecursionError at roughly a thousand levels by default, and some C libraries crash outright. We don’t host pathological files, so generate one in your test suite and confirm you get a clean error:
python3 -c "print('[' * 100000 + ']' * 100000)" > deep.jsonSet an explicit depth limit if your parser offers one, and a request body size limit in front of it either way.
4. Invalid and empty input
Almost-JSON. Invalid JSON uses single quotes, an unquoted key and trailing commas. These are things people write by hand and JavaScript accepts in object literals, but strict JSON parsers reject them. Compare it with the JSON5 config, which uses the same features plus comments and hex numbers on purpose and parses with a JSON5 library. Make sure your error message includes the line and column:
import json
try:
json.load(open("invalid.json"))
except json.JSONDecodeError as e:
print(f"Line {e.lineno}, column {e.colno}: {e.msg}")Empty input. A zero-byte .json file is not valid JSON. JSON.parse('') throws “Unexpected end of JSON input”, and Python reports “Expecting value: line 1 column 1 (char 0)”. Report it as “file is empty”, not as a syntax error. By contrast, the empty array and empty object are valid documents. An import that gets [] should succeed with zero records, not fail.
Wrong file. A JSON array named .csv is a common export mistake. JSON has no magic bytes, so decide whether to sniff for a leading [ or {, or to trust the extension and fail clearly.
5. NDJSON and JSON Lines
Newline-delimited JSON puts one complete value on each line. It is what log shippers, BigQuery exports and LLM fine-tuning APIs use, because you can process it one line at a time in constant memory.
- NDJSON from 1,000 to 100,000 records, plus size-targeted 1 MB, 10 MB and 100 MB files
- JSON Lines: the same records with the
.jsonlextension - Chat fine-tuning JSONL: 200 conversations in the
messageslayout - Structured JSON log: 1,000 log lines, some with a nested
errorobject
Parse line by line, skip blank lines, and report the line number of a bad record instead of failing the whole file:
import json
with open("sample-100k-records.ndjson", encoding="utf-8") as f:
for n, line in enumerate(f, 1):
if not line.strip():
continue
try:
record = json.loads(line)
except json.JSONDecodeError as e:
print(f"Skipping line {n}: {e.msg}")Also test a file with no final newline, one with CRLF line endings, and one with a truncated last line, which is what you get when a writer crashes mid-record.
6. Size: when parse() stops working
JSON.parse and json.load read the whole document into memory, and the parsed objects take several times the file size. Measure with real sizes:
- Exact-size arrays: 1 MB, 5 MB, 10 MB, 50 MB and 100 MB
- Record counts: 1,000, 10,000 and 100,000 records
- Minified JSON: 1,000 records with no whitespace, as APIs return them
/usr/bin/time -l node -e 'JSON.parse(require("fs").readFileSync("sample-100mb.json","utf8"))' # macOS; use -v on LinuxPast a few hundred megabytes, switch to a streaming parser that emits one record at a time: ijson in Python, stream-json in Node, or jq --stream in the shell. Better still, ask producers for NDJSON.
For choosing between JSON, CSV and Parquet in the first place, see CSV vs JSON vs Parquet. All the files above are on the JSON parsing use case.