JSON.parse() and JSON.stringify() look simple. You call one to get data in, call the other to get it out. But both have second and third arguments that most developers never touch — and those arguments unlock features that would otherwise require custom code.
Here are seven things worth knowing.
1. JSON.stringify() has a space argument
Everyone knows the first argument. Most people have no idea about the third:
const data = { name: "Alice", age: 30, tags: ["admin", "editor"] };
// Default — minified
JSON.stringify(data);
// '{"name":"Alice","age":30,"tags":["admin","editor"]}'
// Indented with 2 spaces
JSON.stringify(data, null, 2);
// {
// "name": "Alice",
// "age": 30,
// "tags": [
// "admin",
// "editor"
// ]
// }
// Indented with a tab character
JSON.stringify(data, null, '\t');
The space argument accepts either a number (number of spaces) or a string (used as the indent). This is the only argument you need to produce human-readable JSON for logging, debugging, or config files.
2. JSON.stringify() has a replacer argument
The second argument is a replacer — a way to transform or filter data during serialisation. It can be an array (allowlist of keys) or a function (transform each value):
const user = { id: 1, name: "Alice", password: "secret", token: "xyz" };
// Array replacer — only include these keys
JSON.stringify(user, ["id", "name"], 2);
// { "id": 1, "name": "Alice" }
// Function replacer — strip null values
JSON.stringify({ a: 1, b: null, c: 3 }, (key, value) => {
if (value === null) return undefined; // returning undefined removes the key
return value;
});
// '{"a":1,"c":3}'
// Function replacer — transform dates to ISO strings
JSON.stringify({ createdAt: new Date() }, (key, value) => {
if (value instanceof Date) return value.toISOString();
return value;
});
The function replacer receives every key-value pair, including the root object (key is "" at the root). Return undefined to omit the key.
3. JSON.parse() has a reviver argument
The reviver is the parse-time counterpart to replacer:
const json = '{"createdAt":"2024-01-15T10:30:00.000Z","updatedAt":"2024-02-01T08:00:00.000Z"}';
// Without reviver — dates are strings
JSON.parse(json).createdAt;
// "2024-01-15T10:30:00.000Z" (string, not Date)
// With reviver — auto-convert ISO date strings to Date objects
JSON.parse(json, (key, value) => {
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
// { createdAt: Date object, updatedAt: Date object }
The reviver runs bottom-up — leaf nodes are processed before their parents. This matters when you're transforming nested structures.
4. JSON.stringify() silently drops certain values
This one surprises a lot of people:
const obj = {
a: 1,
b: undefined, // omitted
c: function() {}, // omitted
d: Symbol("x"), // omitted
e: NaN, // becomes null
f: Infinity, // becomes null
g: -Infinity, // becomes null
};
JSON.stringify(obj);
// '{"a":1,"e":null,"f":null,"g":null}'
undefined, functions, and Symbols are silently dropped from objects. In arrays, they become null:
JSON.stringify([1, undefined, function(){}, 4]);
// '[1,null,null,4]'
NaN and Infinity become null — a detail that trips up people working with mathematical data or price calculations in JavaScript.
5. Circular references throw — but you can handle them
const obj = { name: "foo" };
obj.self = obj; // circular reference
JSON.stringify(obj); // throws TypeError: Converting circular structure to JSON
To handle circular references, use a replacer with a WeakSet:
function safeStringify(obj, space) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
}, space);
}
const obj = { name: "foo" };
obj.self = obj;
safeStringify(obj);
// '{"name":"foo","self":"[Circular]"}'
This is useful for logging objects that might have circular references (DOM nodes, some Node.js objects).
6. toJSON() lets objects control their own serialisation
If an object has a toJSON() method, JSON.stringify() calls it and serialises the return value instead:
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
toJSON() {
return `${this.amount} ${this.currency}`; // serialise as a string
}
}
const price = new Money(9.99, "USD");
JSON.stringify({ price }); // '{"price":"9.99 USD"}'
Date objects use this mechanism — Date.prototype.toJSON calls toISOString(), which is why dates serialise to ISO strings automatically. You can use the same pattern for Money, BigInt wrappers, custom types — any class where the default object form isn't the right representation.
7. Deeply nested structures and the performance limit
JSON.parse() and JSON.stringify() are synchronous and run on the main thread. For very large payloads (>1 MB), they can cause UI jank in a browser context. A few strategies:
// For large payloads, consider structuredClone() instead of JSON round-trip for deep cloning
const deep = structuredClone(bigObject); // faster, handles more types, no JSON overhead
// For streaming large JSON (Node.js), use a streaming parser like JSONStream
const JSONStream = require('JSONStream');
fs.createReadStream('large.json').pipe(JSONStream.parse('*')).on('data', row => {
// process one row at a time without loading the whole file
});
// For the browser, consider JSON.parse on a background worker for >5MB files
const worker = new Worker('json-worker.js');
worker.postMessage(largeJsonString);
worker.onmessage = e => console.log(e.data); // parsed object
JavaScript's V8 engine has an effective nesting limit of around 500 levels deep before a stack overflow. In practice, no real-world JSON structure gets close to this, but if you're building a JSON generator, keep this in mind.
Quick Reference
| Argument | JSON.stringify(value, replacer, space) |
|---|---|
replacer |
Array of keys to include, or function to transform each value |
space |
Number of spaces or string to use for indentation |
| Argument | JSON.parse(text, reviver) |
|---|---|
reviver |
Function to transform each parsed value (runs bottom-up) |
If you need to format, validate, or minify JSON without writing any code, the SnappyTools JSON Formatter does it in one click — paste your JSON, get it formatted, validated, and ready to copy. No setup, no signup, runs entirely in your browser.
Top comments (0)