skip to content

JSON.parse accepts a second argument called the reviver. What is it called with, in what order does it walk the parsed result, and what happens when it returns undefined?

level: middleimportance: should knowfreq 46%

answer

  1. parse's mirror of the replacer
  2. key and value, holder as this
  3. children before parents
  4. return nothing and the key is gone
  5. the last call is the whole document

basics

~20 s

The reviver is JSON.parse's second argument, called with each key and value from the innermost values outward. Its return value replaces the parsed one, and returning undefined deletes that property — so it can filter as well as transform.

solid answer

~40 s

After `JSON.parse` builds the plain result, it walks that result and calls your reviver with `(key, value)` for every entry, with `this` bound to the object holding the value. The walk is **bottom-up**: children are revived before their parent, so by the time your callback sees an object, its properties already hold whatever you returned for them. The final call has the empty-string key and the whole root value, and whatever you return there is what `JSON.parse` returns. Returning `undefined` deletes the property from its holder — the classic way to strip fields — while returning any other value substitutes it. That substitution is the standard place to rebuild types the format cannot carry: detect an ISO-8601 string and return `new Date(value)`, or detect your own tagged shape and return the real instance.

code

javascript · 11 lines
javascript
const text = '{"id":1,"createdAt":"2024-05-01T00:00:00.000Z","debug":"noise"}';
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;

const order = JSON.parse(text, (key, value) => {
  if (key === 'debug') return undefined;                 // deletes the property
  if (key === 'createdAt' && ISO.test(value)) return new Date(value);
  return value;
});

console.log(order.createdAt instanceof Date); // true
console.log('debug' in order);                // false

go deeper

for a junior

Know that JSON.parse takes an optional second function argument that can transform each value as it comes back, and that turning date strings into date objects is the usual reason to reach for it.

for a middle

Explain the mechanics: (key, value) with the holder as this, innermost-first traversal, undefined meaning delete, and the final empty-string call whose return value becomes the parse result.

for a senior

Bring the operational judgment — shape-guessing dates converts attacker-chosen strings, the callback runs once per node so cost scales with key count, and it cannot recover precision the text already lost.

for a principal

Own whether ad-hoc revival should exist at all: an agreed tagging convention shared by producer and consumer, applied in one decoding layer, beats a per-call-site callback that silently guesses at types from string shape.

## What the reviver is for Parsing gives you plain objects, arrays and primitives — that is all the format can express. Anything richer that you encoded on the way out has to be rebuilt on the way in, and the reviver is the hook the language gives you at the parse call site. It is the mirror image of the replacer. ## The signature and the binding ```js JSON.parse(text, function (key, value) { // `this` is the object or array that holds `value` under `key` return value; }); ``` The key is always a string, including array indices, which arrive as `'0'`, `'1'` and so on. `this` is the holder, so a regular function (not an arrow) is required whenever your decision depends on a sibling property. ## Bottom-up traversal is the key mechanic The replacer runs top-down; the reviver runs **bottom-up**. Leaves are revived first, then their parent, and the root last: ```js JSON.parse('{"a":{"b":1}}', function (key, value) { console.log(JSON.stringify(key)); return value; }); // "b" // "a" // "" ``` This ordering is what makes reconstruction composable. When your callback is handed the object under key `'a'`, its `b` property already contains whatever you returned for `b`. You can therefore build a rich value out of already-rebuilt children in one pass, without a second traversal. The final call, with key `''`, receives the whole parsed root. Whatever you return from that call is the result of `JSON.parse`, so a reviver can replace the entire document — wrap it, unwrap an envelope, or return a class instance built from it. ## Returning undefined deletes ```js const clean = JSON.parse('{"id":1,"debug":"noise"}', (key, value) => key === 'debug' ? undefined : value ); // { id: 1 } -- 'debug' in clean === false ``` The property is removed from its holder, not set to `undefined`, so an `in` check and `Object.keys` both agree it is gone. Applied to an array element, the deletion leaves a **hole** rather than shifting the later elements down, and the array's length is unchanged — usually not what people intend, so filter arrays after parsing rather than inside the reviver. The same trap as the replacer applies at the root: a reviver that returns `undefined` for keys it does not recognise will also reject the empty-string call and hand you `undefined` for the entire document. ## The canonical use: rebuilding dates Serialization renders a date as an ISO-8601 string, and parsing gives that string straight back. If you want an actual date object you must ask for it: ```js const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/; const order = JSON.parse(text, (key, value) => typeof value === 'string' && ISO.test(value) ? new Date(value) : value ); ``` Note what makes this fragile: the reviver sees only a string, so it is *guessing* from shape. A user-supplied product code that happens to look like a timestamp gets converted too. The robust version keys off the property name, or off an explicit tag written by the producer — `{ "$type": "date", "v": "..." }` — so that reconstruction is driven by an agreed marker rather than by pattern matching untrusted text. ## Rebuilding richer types Because children are already revived, tagged reconstruction composes naturally: ```js const text = '{"tags":{"$type":"set","v":[1,2,2]}}'; const out = JSON.parse(text, (key, value) => value && value.$type === 'set' ? new Set(value.v) : value ); out.tags instanceof Set; // true ``` The array under `v` was revived before the object that wraps it, so the constructor receives a finished value. ## Cost and cautions - The reviver is called **once per node**, so its cost scales with the number of keys in the document, not its byte size. On large payloads a heavy callback — an anchored regular expression against every string, say — is measurable. Compile patterns outside the callback. - It cannot rescue precision that the text never carried. A number written beyond the exactly-representable integer range has already been rounded by the time your callback sees it; only the raw text could have told you, and the reviver's value argument is a number. - It runs after the whole document is parsed and materialised, so it is not a streaming or early-exit mechanism. It cannot reduce peak memory, and it cannot stop parsing partway. - Returning a different object from the root call changes the parse result's type entirely, which is powerful but easy to do accidentally when a broad predicate matches the root.

  • Why does the reviver walk bottom-up while the replacer walks top-down?
    Each order suits its direction of work. Serializing needs to decide what a container becomes *before* descending, because the substitute is what gets walked. Reconstruction needs the opposite: to rebuild a rich value from its children, the children must already be finished. Bottom-up means that when your callback receives an object, every property already holds your revived version, so one pass suffices.
  • What goes wrong if a reviver deletes an array element by returning undefined?
    The element is deleted rather than removed, leaving a hole: the length is unchanged and the index reads as `undefined` with `in` returning false. Iteration methods behave inconsistently across holes, so the array is now subtly awkward. Filter arrays after parsing — the reviver's delete semantics are designed for object properties.
  • Is guessing dates from string shape a safe way to revive them?
    No — the reviver only sees the parsed string, so a shape test converts anything that happens to match, including user-supplied text. Prefer keying off the property name you control, or have the producer write an explicit tag such as `{"$type":"date"}` so reconstruction is driven by an agreed marker rather than by pattern-matching untrusted content.

saying these in an interview costs you the question

  • Thinks the reviver runs before or during parsing
  • Expects parents to be visited before their children
  • Believes returning undefined sets the property to undefined
  • Forgets the final call receives the whole document
  • Assumes the reviver can recover a number's original text precision

context