JSON.stringify(value, replacer, space) walks the value and, for each property, runs one fixed pipeline (the spec's SerializeJSONProperty):
- If the value is an object or a BigInt, call its
toJSON(key)method if it has one.Date.prototype.toJSONis why dates become ISO strings. - If a replacer function was given, call it with
thisset to the holder object, passing(key, value). - Unwrap
Number,String,Boolean, andBigIntwrapper objects to their primitives. - Serialize by type.
What survives step 4:
| Value | In an object | In an array | At top level |
|---|---|---|---|
undefined, function, symbol |
property omitted | null |
returns undefined (not a string) |
NaN, Infinity, -Infinity |
null |
null |
"null" |
-0 |
0 |
0 |
"0" |
| BigInt | throws TypeError |
throws | throws |
| Cycle | throws TypeError |
throws | throws |
Only an object's own enumerable string-keyed properties are written (integer keys first, then insertion order). Symbol keys, inherited properties, non-enumerable properties, and #private fields are skipped. Map, Set, RegExp, and Error have no such properties, so they come out as {}. Prototype getters are not written.
The replacer can be a function (called for every key, root first with key "") or an array of strings and numbers that acts as an allow-list for object keys. An array replacer does not filter array elements, and applies at every depth. Returning undefined from the function drops the property.
space (a number, capped at 10, or a string truncated to its first 10 characters) turns on indentation.
Strings are escaped per QuoteJSONString. Since the well-formed JSON.stringify change (ES2019), a lone surrogate is written as an escape like \ud800 instead of a broken character.