JSON.stringify and JSON.parse Explained, Including the Surprises

By Sheng Pang · Published · 3 min read

Every JavaScript developer uses these two functions daily, and most have been bitten by at least one of their quirks. This guide covers the full signatures, then the behaviours that catch people out.

JSON.stringify

JSON.stringify(value, replacer, space)

Turns a JavaScript value into a JSON string.

JSON.stringify({ a: 1, b: [true, null] });
// '{"a":1,"b":[true,null]}'

JSON.stringify({ a: 1, b: 2 }, null, 2);
// '{\n  "a": 1,\n  "b": 2\n}'   pretty printed with 2 spaces

JSON.stringify({ a: 1, b: 2 }, ["a"]);
// '{"a":1}'   only the listed keys

JSON.stringify(obj, (key, val) => typeof val === "bigint" ? val.toString() : val);
// replacer function transforms each value

The third argument can also be a string such as "\t" to indent with tabs. Anything longer than ten characters is cut to ten.

JSON.parse

JSON.parse(text, reviver)

Turns a JSON string into a value. It throws a SyntaxError on invalid input, so wrap it in try and catch when the text comes from outside.

JSON.parse('{"when":"2026-09-22T10:00:00Z"}', (key, val) =>
  key === "when" ? new Date(val) : val
);
// { when: Date object }   reviver rebuilds types

The surprises

undefined, functions and symbols vanish

JSON.stringify({ a: undefined, b: () => {}, c: Symbol() });
// '{}'

JSON.stringify([undefined, () => {}]);
// '[null,null]'   in arrays they become null instead

This is why an object can lose keys on a round trip through JSON.

Dates become strings and stay strings

JSON.stringify({ d: new Date(0) });
// '{"d":"1970-01-01T00:00:00.000Z"}'

JSON.parse('{"d":"1970-01-01T00:00:00.000Z"}').d
// the string, not a Date

Use a reviver or convert after parsing. This trips up nearly everyone once.

NaN and Infinity become null

JSON.stringify({ n: NaN, i: Infinity });
// '{"n":null,"i":null}'

BigInt throws

JSON.stringify({ big: 10n });
// TypeError: Do not know how to serialize a BigInt

Convert to a string in a replacer, or add a toJSON method to BigInt.prototype.

Large numbers lose precision

JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992

JavaScript numbers are 64 bit floats and cannot represent integers above 2 to the 53 exactly. If an API sends 64 bit IDs as numbers, they will be silently rounded. Ask for them as strings, or use a parser that supports BigInt.

Circular references throw

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

toJSON is called if present

const user = { name: "Ada", password: "x", toJSON() { return { name: this.name }; } };
JSON.stringify(user);
// '{"name":"Ada"}'

Date uses this mechanism internally. It is the cleanest way to control how a class serialises.

Key order is preserved, mostly

JavaScript keeps insertion order for string keys, so stringify outputs them in that order. Integer like keys such as "1" are always output first in numeric order regardless of when they were added. To get a stable output for comparison, sort the keys first. Our JSON formatter has a Sort Keys button that does this recursively.

The deep clone trick and its replacement

const copy = JSON.parse(JSON.stringify(obj));

This has been the go to deep copy for years. It works for plain data but drops everything listed above: dates become strings, undefined vanishes, maps and sets become empty objects. The modern replacement is structuredClone(obj), available in all current browsers and Node 17 and later. It handles dates, maps, sets, typed arrays and circular references correctly.

Quick reference

Valuestringify result
undefined in objectkey omitted
undefined in arraynull
functionomitted or null
DateISO string
NaN, Infinitynull
BigIntthrows
Map, Set{}
circular objectthrows

← Back to all articles