JSON.parse/stringify edge cases, streaming JSON for large payloads, JSON Schema with Ajv, MessagePack as a binary alternative, and the serialization cost at scale.
Module P-13 — JSON Internals, Serialization, and Schema Validation
What this module covers: JSON is everywhere in Node.js — request bodies, response payloads, configuration files, inter-service communication. Most developers treat it as a black box and pay for it later with silent data loss, unexpected type coercions, and performance problems at scale. This module covers what
JSON.parseandJSON.stringifyactually do under the hood, the edge cases that corrupt data silently, streaming JSON for payloads too large to hold in memory, fast serialization withfast-json-stringify, JSON Schema validation with Ajv, and when MessagePack is worth the switch.
What JSON.parse and JSON.stringify Actually Do
JSON is a text format. JSON.parse converts a string into a JavaScript object. JSON.stringify does the reverse. The key word is string — everything must pass through text.
This matters because JavaScript has types that JSON doesn't:
| JavaScript type | JSON result | What happens |
|---|---|---|
undefined | omitted | property disappears |
NaN | "null" | silently becomes null |
Infinity | "null" | silently becomes null |
Date | "2024-05-22T14:32:01.123Z" | becomes a string, not a Date on parse |
BigInt | throws TypeError | crashes your process |
Map, Set | {} | serialised as empty objects |
Symbol | omitted | property disappears |
| Circular reference | throws TypeError | crashes your process |
JSON.stringify silently dropping undefined and turning NaN/Infinity into null is like a customs form that quietly discards anything it doesn't have a box for — nothing errors, the paperwork just arrives incomplete.
Silent Precision Loss: Large Numbers Without BigInt
The table above shows BigInt throwing on JSON.stringify — a loud, immediate failure you can't miss. A plain number above Number.MAX_SAFE_INTEGER is the dangerous version of the same problem, because it doesn't throw at all:
A token-transfer indexer once stored raw wei amounts as a plain JS number instead of BigInt or a string. Every time a transfer record round-tripped through an internal message queue as JSON, any amount above the safe-integer threshold silently rounded to the nearest representable double. The drift was only a few hundred wei per affected value — invisible in unit tests built around small fixture numbers — until a production reconciliation audit found balances that didn't match the chain, well after the rounding had been happening quietly in production for a while.
Large numeric IDs and monetary amounts (wei, satoshis, cents at scale) should be BigInt or a string from the moment they're read out of the database — never a plain number that happens to fit in dev with smaller test fixtures.
Buffer Serialization: The Accidental Leak
Buffer doesn't throw and doesn't silently vanish like undefined — it silently expands into a verbose, easy-to-miss shape:
Every byte becomes a decimal number in an array. This bites in practice when a field that's a Buffer under the hood — a Prisma Bytes column, the output of crypto.randomBytes(), a file read with fs.readFileSync() without an encoding — passes straight into res.json() without anyone noticing its real type:
Nothing errors. The response just gets bigger, uglier, and — if the buffer held anything sensitive (key material, a password hash, an internal token) — leakier than intended. The fix is the same discipline as any other field you don't want serialised as-is: convert explicitly (apiKeyHash.toString('base64')) or exclude it via the replacer whitelist pattern shown below.
structuredClone(): The Modern Alternative to JSON Round-Tripping
Deep-cloning with JSON.parse(JSON.stringify(x)) inherits every problem in this section — Dates become strings, Map/Set become {}, undefined disappears. Node 17+ ships a built-in structuredClone() that performs a real deep clone using the structured clone algorithm instead of a JSON round-trip:
structuredClone even handles circular references natively — something JSON.stringify can't do at all without the WeakSet workaround below. It does have limits: it throws on functions, and cloning a custom class instance gives you back a plain object with the same own properties, not an instance of that class — the prototype chain doesn't survive. For plain data (API payloads, config objects, anything already JSON-shaped), it's the better default; reach for a real cloning library only when you need class instances or functions preserved too.
Safe Patterns
Handling BigInt
BigInt appears frequently when working with PostgreSQL's BIGSERIAL primary keys or blockchain IDs that exceed Number.MAX_SAFE_INTEGER (2⁵³ - 1 ≈ 9 quadrillion):
Handling Dates
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion