JSON parse and stringify pitfalls
JSON.stringify/parse footguns — undefined, dates, NaN, cycles, toJSON, revivers, and safe parsing of untrusted input.
- javascript
- json
- parse
- stringify
JSON is the lingua franca of APIs and localStorage. It is not a full clone of JavaScript values. Most “mystery data loss” bugs are stringify dropping fields you assumed would survive.
What survives
JSON.stringify({
n: 1,
s: 'ok',
b: true,
arr: [1, null, 'x'],
nested: { a: 1 },
});
Numbers, strings, booleans, null, arrays, plain objects. Keys become strings.
What dies or mutates
JSON.stringify({
u: undefined, // key omitted
fn: () => {}, // key omitted
sym: Symbol('x'), // key omitted
nan: NaN, // null
inf: Infinity, // null
date: new Date(), // ISO string, not Date
map: new Map([['a', 1]]), // {}
set: new Set([1]), // {}
});
JSON.stringify([undefined, () => {}]); // "[null,null]"
const a = {};
a.self = a;
JSON.stringify(a); // TypeError: cyclic structure
toJSON
const money = {
cents: 199,
toJSON() {
return { usd: (this.cents / 100).toFixed(2) };
},
};
JSON.stringify(money); // '{"usd":"1.99"}'
Date.prototype.toJSON explains the ISO string conversion.
replacer and space
JSON.stringify(obj, ['id', 'name']); // whitelist keys
JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value,
);
JSON.stringify(obj, null, 2); // pretty print
parse and reviver
JSON.parse('{"a":1}'); // { a: 1 }
JSON.parse('x'); // SyntaxError
const data = JSON.parse('{"when":"2026-08-04T00:00:00.000Z"}', (key, value) => {
if (key === 'when') return new Date(value);
return value;
});
Safe parse helper
function safeJsonParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
// localStorage
const prefs = safeJsonParse(localStorage.getItem('prefs'), {});
Never JSON.parse without handling SyntaxError for external/storage input.
Security
JSON.parse does not execute code (unlike eval). Still:
- Validate shape after parse (schema, zod, manual checks)
- Don’t assign parsed HTML to
innerHTML - Prototype pollution: careful merges of objects with
__proto__keys from untrusted JSON (useObject.create(null)or null-prototype merges)
const raw = JSON.parse('{"__proto__":{"polluted":true}}');
// merging into {} with broken libs can pollute — use safe merge
Deep clone myth
const clone = JSON.parse(JSON.stringify(obj)); // lossy!
Prefer structuredClone for clones; JSON only for JSON-shaped data.
Interview answer
“JSON supports a subset of JS values. stringify drops undefined/functions/symbols, turns NaN/Infinity into null, Dates into strings, and throws on cycles. parse throws SyntaxError on bad input—always try/catch for external data. I use reviver/replacer carefully and never treat JSON as a general deep clone.”
Related
Pretty print and stable keys
// deterministic stringify for hashing/cache keys (simple version)
function stableStringify(value) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
const sorted = Object.keys(value)
.sort()
.reduce((acc, k) => {
acc[k] = value[k];
return acc;
}, {});
return JSON.stringify(sorted, (_, v) =>
typeof v === 'object' && v && !Array.isArray(v)
? JSON.parse(stableStringify(v))
: v,
);
}
return JSON.stringify(value);
}
JSON object key order is insertion order in modern engines, but producers differ — stabilize when the string is used as a cache key. For logs, JSON.stringify(err) remains useless; pick fields. For config files, prefer parsers that allow comments only if you control the format — standard JSON.parse rejects comments.
Further reading
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.