Module P-13·20 min read

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.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

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.parse and JSON.stringify actually do under the hood, the edge cases that corrupt data silently, streaming JSON for payloads too large to hold in memory, fast serialization with fast-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 typeJSON resultWhat happens
undefinedomittedproperty 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
BigIntthrows TypeErrorcrashes your process
Map, Set{}serialised as empty objects
Symbolomittedproperty disappears
Circular referencethrows TypeErrorcrashes your process
typescript

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:

typescript

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:

typescript

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:

typescript

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:

typescript

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):

typescript

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 & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.