JavaScript JSON Deep Dive: parse, stringify, Reviver/Replacer, and the Edge Cases

Link copied
JavaScript JSON Deep Dive: parse, stringify, Reviver/Replacer, and the Edge Cases

JavaScript JSON Deep Dive: parse, stringify, Reviver/Replacer, and the Edge Cases

JSON is the lingua franca of every API, every config file, every browser-to-server message. JavaScript's JSON.parse and JSON.stringify are the two functions you'll call thousands of times in any codebase. Both look simple — they're not. There are edge cases around undefined, dates, functions, circular references, and big numbers that bite production systems regularly.

This lesson walks through both methods, the reviver and replacer callbacks (their less-known second arguments), JSON Lines for streaming, and the patterns for handling the edge cases without surprising losses.

What JSON is and isn't #

JSON (JavaScript Object Notation) is a subset of JavaScript object literal syntax. The data types it supports:

  • null
  • booleans (true, false)
  • numbers (any IEEE 754 representable float — no NaN, Infinity)
  • strings (UTF-16, with escape sequences)
  • arrays
  • objects (with string keys only)

What it does NOT support:

  • undefined
  • NaN, Infinity, -Infinity
  • Date, Map, Set, RegExp, Function, Symbol, BigInt
  • Comments
  • Trailing commas
  • Unquoted keys

Knowing this list is the foundation for every edge case below.

JSON.stringify — JavaScript → JSON string #

JSON.stringify({ a: 1, b: 'two' });
// '{"a":1,"b":"two"}'

JSON.stringify({ a: 1, b: 'two' }, null, 2);
// '{\n  "a": 1,\n  "b": "two"\n}' — pretty-printed with 2-space indent

JSON.stringify(['a', 'b'], null, '\t');
// tab-indented

Signature: JSON.stringify(value, replacer, space).

  • value — anything; the top-level value to serialize.
  • replacernull or an array of keys to include, or a function for fine-grained control.
  • spacenull for compact, a number 1–10 for that many spaces, or a string (often '\t') for indent.

Values that get dropped #

undefined, function, and symbol are silently dropped from objects and turned into null in arrays:

JSON.stringify({ a: 1, b: undefined, c: () => {}, d: Symbol() });
// '{"a":1}'   — b, c, d all dropped

JSON.stringify([1, undefined, () => {}]);
// '[1,null,null]'

This is the most common source of "why is my field missing?" bugs. If you serialize a user object and user.email === undefined, the JSON won't have an email field at all.

Special-cased values #

JSON.stringify(NaN);          // 'null' — NOT '"NaN"'
JSON.stringify(Infinity);     // 'null'
JSON.stringify(new Date());   // '"2026-05-21T14:30:00.000Z"' — toISOString is auto-called
  • NaN and Infinity become null — silently lossy. Sanitize before serializing if these can appear.
  • Date is auto-stringified via its toJSON() method (which calls toISOString()).
  • BigInt throws — there's no JSON representation. Convert to string first.

Custom toJSON() #

Any object can define its own toJSON() method to control how JSON.stringify serializes it:

class Money {
  constructor(cents) { this.cents = cents; }
  toJSON() {
    return { amount: this.cents / 100, currency: 'USD' };
  }
}

JSON.stringify(new Money(1999));
// '{"amount":19.99,"currency":"USD"}'

Useful for classes that should serialize differently than their internal layout.

The replacer callback #

A function called for every key-value pair:

const obj = { name: 'Ada', password: 'secret', age: 36 };

JSON.stringify(obj, (key, value) => key === 'password' ? undefined : value);
// '{"name":"Ada","age":36}' — password removed

Returning undefined from the replacer omits the key. Returning anything else replaces the value.

Use cases:

  • Strip sensitive fields (passwords, tokens, PII)
  • Convert problematic types (BigInt → string)
  • Trim large arrays for logging
function safeReplacer(key, value) {
  if (typeof value === 'bigint') return value.toString();
  if (key === 'password' || key === 'token') return undefined;
  return value;
}

The replacer-as-array form #

If you pass an array of strings instead of a function, only those keys are included:

JSON.stringify({ a: 1, b: 2, c: 3 }, ['a', 'c']);
// '{"a":1,"c":3}'

Allowlist-style. Doesn't traverse arrays or recurse — just filters the keys at every level.

JSON.parse — JSON string → JavaScript #

JSON.parse('{"a":1}');     // { a: 1 }
JSON.parse('[1,2,3]');     // [1, 2, 3]
JSON.parse('null');         // null
JSON.parse('"hi"');         // 'hi'
JSON.parse('42');           // 42

Signature: JSON.parse(text, reviver).

Throws SyntaxError for invalid JSON:

try {
  JSON.parse('{a: 1}');  // unquoted keys — invalid JSON
} catch (e) {
  console.log(e.message); // 'Unexpected token a in JSON at position 1'
}

Always wrap parsing of untrusted input in a try/catch.

The reviver callback #

The inverse of the replacer. Called for every key-value pair during parsing, bottom-up:

const json = '{"createdAt":"2026-05-21T14:30:00Z","count":1}';
const obj = JSON.parse(json, (key, value) => {
  if (key === 'createdAt' && typeof value === 'string') {
    return new Date(value);
  }
  return value;
});
obj.createdAt instanceof Date;  // true

Returning undefined removes the key. Returning anything else uses that as the value.

Use cases:

  • Convert ISO date strings back into Date objects
  • Parse strings into BigInts (for large IDs)
  • Validate as you parse, throwing on bad shapes

Common edge cases #

Big numbers (the JSON "number" problem) #

JSON numbers are parsed as JavaScript numbers — which means precision loss past 2⁵³ – 1 (Lesson 8.2).

JSON.parse('9007199254740993');
// 9007199254740992 — lost precision

For large IDs, the workaround is to send them as JSON strings:

{ "userId": "9007199254740993" }

Then BigInt(parsed.userId) on the JavaScript side.

Note: in 2025/26, native JSON BigInt support is in TC39 (the JSON.parse({ reviver }) proposal lets you opt into BigInt for specific keys), but it's not yet universal. For now, stringify big numbers at the API layer.

Circular references #

JSON.stringify throws on a cycle:

const a = {};
a.self = a;
JSON.stringify(a);
// TypeError: Converting circular structure to JSON

For logging or debugging, use a circular-safe stringifier:

function safeStringify(obj, space = 2) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  }, space);
}

Libraries like safe-stable-stringify do this with extra features (consistent key order for hashing, depth limits, etc.).

Class instances #

JSON.stringify doesn't preserve class identity:

class User { constructor(n) { this.name = n; } }
const u = new User('Ada');
const json = JSON.stringify(u);          // '{"name":"Ada"}'
const back = JSON.parse(json);            // { name: 'Ada' } — plain object, NOT a User
back instanceof User;                      // false

The roundtrip preserves data but not behavior. For serialization that survives class identity, use a serialization library (superjson, class-transformer) or implement toJSON/fromJSON patterns yourself.

Map and Set #

Maps and Sets stringify to empty objects/arrays:

JSON.stringify(new Map([['a', 1]])); // '{}'
JSON.stringify(new Set([1, 2, 3]));   // '{}'

Convert to arrays first:

JSON.stringify([...map]);           // '[["a",1]]'
JSON.stringify([...set]);            // '[1,2,3]'

JSON Lines / NDJSON #

For streaming or log files, JSON Lines is one JSON value per line:

{"event":"login","user":1,"at":"2026-05-21T14:30:00Z"}
{"event":"page","user":1,"url":"/home"}
{"event":"logout","user":1,"at":"2026-05-21T14:35:00Z"}

Not valid as a single JSON document, but trivially parseable line by line:

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

const stream = createReadStream('events.jsonl');
for await (const line of createInterface(stream)) {
  if (line.trim()) {
    const event = JSON.parse(line);
    process(event);
  }
}

Log aggregators (Datadog, Loki), data pipelines, and AI APIs all use JSON Lines.

Pretty-printing in production #

A tiny but useful pattern:

function toReadableJson(value) {
  return JSON.stringify(value, null, 2);
}

For in-app debugging, use console.log(obj) (DevTools shows it as a tree) rather than console.log(JSON.stringify(obj)). Strings lose the inspection affordances.

A summary #

  • JSON is JavaScript objects minus undefined, classes, dates, functions, NaN/Infinity, and BigInt.
  • stringify(value, replacer, space) — replacer strips/transforms, space pretty-prints.
  • parse(text, reviver) — reviver lets you reconstitute non-JSON types (Dates, BigInts).
  • Wrap parse in try/catch for untrusted input.
  • undefined, functions, symbols are dropped from objects. Become null in arrays.
  • NaN and Infinity become null. Sanitize if these can appear.
  • Big integers lose precision — send as strings, parse back to BigInt if needed.
  • Class instances lose their typeinstanceof returns false after roundtrip.
  • Maps and Sets must be converted to arrays first.
  • JSON Lines (NDJSON) is the standard for log streams.

What's next #

That completes Module 8 — the Standard Library Deep Dive. Module 9 covers regular expressions from the syntax basics through modern features (lookaheads, named groups, Unicode) and real-world patterns (URL parsing, validation, find-and-replace).

Try it yourself #

The undefined-dropping behavior is the easiest one to feel. Predict the result:

YouPredict the output:
const data = {
name: 'Ada',
email: undefined,
joined: new Date('2026-01-01'),
score: NaN,
};
console.log(JSON.stringify(data));
Claude · used js_sandboxOutput: {"name":"Ada","joined":"2026-01-01T00:00:00.000Z","score":null}.

Three things happened silently:
email: undefined was dropped entirely — the field disappears.
joined (a Date) was auto-stringified via its toJSON() method.
score: NaN became null.

If your API contract expects an email field always present, the round-trip lost it. Sanitize undefineds before stringify (set them to null explicitly) when nulls are meaningful in your protocol.

Knowing exactly what gets dropped or transformed is the single most useful piece of JSON knowledge for real-world API work.

Up next in JavaScript

More from this topic

View all JavaScript articles →

Enjoyed this article?

Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email address will not be published. Required fields are marked *