JSON.parse() with reviver
ES5+The reviver parameter of JSON.parse() runs on every key-value pair as the text is parsed, from the deepest leaves upward, letting you replace parsed values on the fly. It is the standard hook for restoring Dates, BigInts, Maps, and other types that JSON flattens into strings and arrays.
Syntax
JSON.parse(text, reviver)Parameters
text string The string to parse as JSON
reviver Function A function that receives key and value, and returns the transformed value
Return Value
The transformed JavaScript value
Examples
const json = '{"created":"2024-01-15","count":"42"}';
const obj = JSON.parse(json, (key, value) => {
if (key === 'created') return new Date(value);
if (key === 'count') return parseInt(value, 10);
return value;
});
console.log(obj.created instanceof Date);
console.log(typeof obj.count); 📌 When to Use
Use a reviver when parsed JSON should arrive already converted into the types your code actually works with, instead of scattering conversions across every consumer. Classic conversions: ISO-8601 strings back into Date objects, decimal strings into numbers or BigInts (backends often send money and 64-bit IDs as strings precisely so they survive JSON), tagged wrappers like { "$type": "Map", "entries": [...] } back into real Maps and Sets, and enum strings into richer constant objects. Because the reviver visits leaves before parents (bottom-up), a parent object already sees its children revived, which makes hierarchical reconstruction possible — you can rebuild a tree of class instances in one pass. Revivers also serve as parse-time filters: returning undefined deletes a property, a blunt but effective way to drop dangerous keys such as __proto__ or constructor from untrusted payloads before they ever reach merging logic. Prefer post-parse transformation of a few known fields when the schema is small and fixed; prefer a reviver when conversions are type-driven, appear throughout the tree, or must apply uniformly to every payload passing through a shared API client.
⚠️ Common Mistakes
Reviving dates too aggressively - a loose pattern like /\d{4}-\d{2}-\d{2}/ also matches order numbers, version strings, and address lines, silently turning them into Dates. Anchor patterns to the full ISO shape, validate with isNaN(date.getTime()), or better, convert only known field names.
Throwing away the this context - inside a non-arrow reviver, this is the object holding the current property, which is exactly what you need to inspect sibling values or a $type tag when deciding how to revive; an arrow function discards that context permanently.
Filtering array elements by returning undefined - for arrays this deletes the index rather than reindexing, producing a sparse array with holes that map() skips and JSON.stringify() turns into null. Filter arrays after parsing instead of inside the reviver.
Not returning the value from the reviver, which replaces it with undefined.
Modifying nested objects before their children are processed - reviver processes bottom-up.
✅ Best Practices
Centralize revival in your API client so every response passes through one reviver - types stay consistent app-wide and new endpoints inherit correct Dates and BigInts for free, instead of each feature re-implementing its own conversion layer.
Use tagged serialization for round-tripping rich types: emit { "$type": "Set", "values": [...] } from a matching replacer, and have the reviver reconstruct by tag - an explicit, debuggable protocol beats guessing types from string shapes.
Harden parsers of untrusted input by dropping dangerous keys in the reviver: if (key === "__proto__" || key === "constructor") return undefined - a cheap defense-in-depth layer against prototype-pollution payloads aimed at naive merge utilities downstream.
Use ISO 8601 date format detection: /^\d{4}-\d{2}-\d{2}T/.test(value) for reliable date parsing.
⚡ Performance Notes
A reviver turns native-speed parsing into a hybrid: the JSON text is still parsed natively, but your function then runs once per node — every key of every object, every element of every array, plus the root. On large payloads this frequently costs more than the parse itself, and the price is proportional to what the reviver does: a typeof check is nearly free, a regex test on every string is noticeable, and constructing objects (new Date) allocates on top. Optimize by short-circuiting early — check typeof value === "string" and a plausible length before running the regex — precompiling patterns outside the call, and keying conversions on known field names instead of inspecting every value. For megabyte-scale responses where only a handful of fields need conversion, skip the reviver and convert those fields after parsing, or move the whole parse-plus-revive step into a Web Worker to keep the UI responsive.
🌍 Real World Example
Type-Safe JSON Parser
This typedParse() helper builds a reusable, type-aware parser. One branch revives ISO timestamp strings into Date objects, guarding with an anchored regex plus an isNaN(getTime()) validity check so near-miss strings pass through untouched; the other converts known numeric field names — count, total, amount — from the string form many APIs use into real numbers. The result: every caller receives ready-to-use types with conversion logic defined exactly once, the pattern you would embed in a shared API client module.
function typedParse(jsonString) {
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
return JSON.parse(jsonString, (key, value) => {
// Convert ISO date strings to Date objects
if (typeof value === 'string' && isoDatePattern.test(value)) {
const date = new Date(value);
return isNaN(date.getTime()) ? value : date;
}
// Convert numeric strings (from certain APIs)
if (key === 'count' || key === 'total' || key === 'amount') {
const num = Number(value);
return isNaN(num) ? value : num;
}
return value;
});
}
const json = '{"created":"2024-01-15T10:30:00Z","count":"42","name":"Test"}';
const parsed = typedParse(json);
console.log(parsed.created instanceof Date); // true
console.log(typeof parsed.count); // "number"