JSON.stringify(value, replacer, space) — the third argument, space, controls indentation: pass 2 for 2-space indent, 4 for 4-space, or '\t' for tabs. The second argument, replacer, filters which keys are included. Pass null to include all keys. Most developers only use JSON.stringify(data, null, 2) for pretty-printing.
JSON.stringify(value, replacer, space)
The space parameter — controlling indentation
The third argument, space, controls the indentation of the formatted output. It accepts either a number or a string:
Number — spaces per indent level
const data = { name: "Alice", address: { city: "London", zip: "SW1A" } };
// No indent (minified) — default when space is 0, null, or omitted
JSON.stringify(data);
// {"name":"Alice","address":{"city":"London","zip":"SW1A"}}
// 2-space indent
JSON.stringify(data, null, 2);
// {
// "name": "Alice",
// "address": {
// "city": "London",
// "zip": "SW1A"
// }
// }
// 4-space indent
JSON.stringify(data, null, 4);
// {
// "name": "Alice",
// "address": {
// "city": "London",
// "zip": "SW1A"
// }
// }
Values larger than 10 are clamped to 10. Negative values are treated as 0 (minified output).
String — custom indent character
// Tab indentation (useful for some file formats)
JSON.stringify(data, null, '\t');
// {
// <tab>"name": "Alice",
// <tab>"address": {
// <tab><tab>"city": "London",
// <tab><tab>"zip": "SW1A"
// <tab>}
// }
// Custom string (first 10 chars only, per spec)
JSON.stringify(data, null, '--');
// {
// --"name": "Alice",
// --"address": {
// ----"city": "London",
// ----"zip": "SW1A"
// --}
// }
The replacer parameter — filtering and transforming output
The second argument controls which keys appear in the output and what their values are. It can be an array (allowlist) or a function (transformation).
Replacer as an array — include only specific keys
const user = {
id: 42,
name: "Alice",
email: "alice@example.com",
password: "secret123",
createdAt: "2026-05-04"
};
// Only include id and name in the output
JSON.stringify(user, ['id', 'name'], 2);
// {
// "id": 42,
// "name": "Alice"
// }
Replacer as a function — exclude or transform values
// Exclude sensitive fields
JSON.stringify(user, (key, value) => {
if (key === 'password') return undefined; // omit this key
return value; // include everything else
}, 2);
// Exclude multiple sensitive fields
const sensitiveKeys = new Set(['password', 'ssn', 'creditCard']);
JSON.stringify(user, (key, value) => {
return sensitiveKeys.has(key) ? undefined : value;
}, 2);
// Transform values — convert Date objects to ISO strings
const event = {
title: "Conference",
date: new Date('2026-06-15'),
attendees: 250
};
JSON.stringify(event, (key, value) => {
if (value instanceof Date) return value.toISOString();
return value;
}, 2);
// {
// "title": "Conference",
// "date": "2026-06-15T00:00:00.000Z",
// "attendees": 250
// }
Important: how the replacer function is called
The replacer function is called with the key and value for every key-value pair, including the root object. For the root, the key is an empty string "". If the replacer returns undefined for any key, that key is omitted from the output. If it returns undefined for the root (empty key), the entire output is undefined.
JSON.stringify({ a: 1, b: 2 }, (key, value) => {
console.log(JSON.stringify(key), '→', value);
return value;
});
// "" → { a: 1, b: 2 } (root call, key is empty string)
// "a" → 1
// "b" → 2
toJSON() — custom serialization on objects
If an object has a toJSON() method, JSON.stringify() calls it and serializes the returned value instead of the object itself:
class User {
constructor(id, name, password) {
this.id = id;
this.name = name;
this.password = password;
}
// This controls what JSON.stringify serializes
toJSON() {
return { id: this.id, name: this.name }; // omit password
}
}
const user = new User(1, 'Alice', 'secret');
JSON.stringify(user); // '{"id":1,"name":"Alice"}'
Values that JSON.stringify() silently drops
This is the most common source of serialization data-loss bugs. JSON has no representation for several JavaScript types, and JSON.stringify() handles them without any error or warning — data simply disappears.
| Value type | In an object property | In an array |
|---|---|---|
undefined | Property omitted entirely | Converted to null |
function | Property omitted entirely | Converted to null |
Symbol value | Property omitted entirely | Converted to null |
| Symbol-keyed property | Always omitted | N/A |
NaN | Converted to null | Converted to null |
Infinity | Converted to null | Converted to null |
BigInt | Throws TypeError: Do not know how to serialize a BigInt | |
Date | Calls .toJSON() → ISO 8601 string (survives, but type is lost) | |
Map, Set | Converted to {} — all data lost silently | |
| Circular reference | Throws TypeError: Converting circular structure to JSON | |
const data = {
name: "Alice",
score: NaN, // → null
rank: Infinity, // → null
fn: () => {}, // → omitted
tags: [1, undefined, 3] // undefined in array → null
};
JSON.stringify(data);
// '{"name":"Alice","score":null,"rank":null,"tags":[1,null,3]}'
// BigInt — fix with a replacer
const safeReplacer = (key, val) =>
typeof val === "bigint" ? val.toString() : val;
JSON.stringify({ count: 9007199254740993n }, safeReplacer);
// '{"count":"9007199254740993"}'
JSON.parse() and the reviver function
The reviver is the second argument to JSON.parse(text, reviver) — the exact counterpart to the replacer in JSON.stringify(). It is called for every key-value pair in the parsed result, from the deepest nested values up to the root. Whatever it returns replaces that value.
The most common use is restoring Date objects. JSON has no native date type, so dates serialize to ISO 8601 strings. Without a reviver, JSON.parse() gives you a plain string. With a reviver, you convert it back to a Date instance during parsing.
const json = '{"name":"Alice","createdAt":"2026-01-15T09:00:00.000Z","score":42}';
// Without reviver — createdAt is a plain string
const plain = JSON.parse(json);
console.log(typeof plain.createdAt); // "string"
// With reviver — createdAt becomes a Date instance
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
const parsed = JSON.parse(json, (key, value) => {
if (typeof value === "string" && ISO_DATE.test(value)) {
return new Date(value);
}
return value;
});
console.log(parsed.createdAt instanceof Date); // true
console.log(parsed.createdAt.getFullYear()); // 2026
Reviver call order: children before parents. By the time the reviver sees an object, all its nested values have already been processed. The very last call has an empty string key and the fully-transformed root value.
The JSON.parse(JSON.stringify(obj)) deep-clone pattern
This one-liner is widely used for deep-cloning plain objects. It works correctly for objects containing only JSON-safe types (strings, numbers, booleans, null, nested plain objects, and arrays of those).
const original = { name: "Alice", address: { city: "London" } };
const clone = JSON.parse(JSON.stringify(original));
clone.address.city = "Paris";
console.log(original.address.city); // "London" — true deep clone
What it silently destroys:
undefinedproperties — dropped, not in the cloneDateinstances — become ISO 8601 strings in the cloneMapandSet— become empty objects{}- Class instances — prototype chain is lost; clone is a plain object
- Circular references — throw
TypeErrorimmediately
For modern environments (Node.js 17+, Chrome 98+, Firefox 94+), use structuredClone() instead. It correctly handles Date, Map, Set, ArrayBuffer, and circular references:
const obj = {
date: new Date("2026-01-15"),
map: new Map([["key", "value"]]),
nested: { arr: [1, 2, 3] }
};
const clone = structuredClone(obj);
console.log(clone.date instanceof Date); // true — Date preserved
console.log(clone.map.get("key")); // "value" — Map preserved
Browser alternative for one-off formatting
If you need to format a JSON string without writing code, paste it into the JSON Formatter. It supports 2-space and 4-space indentation, a tree view for navigation, and key sorting — equivalent to JSON.stringify(data, null, 2) with the sort-keys option.
Frequently Asked Questions
What does the third argument to JSON.stringify do?
The third argument (space) controls indentation. Pass a number for that many space characters per indent level (clamped to 10), or a string to use that string as the indent (e.g. '\t' for tabs). Omitting it or passing null produces compact output with no whitespace.
What is the replacer argument in JSON.stringify?
The second argument (replacer) can be an array or a function. An array of key names acts as an allowlist — only those keys appear in the output. A function is called for every key-value pair; whatever it returns is serialized, and returning undefined omits the property. Pass null to include all keys unchanged.
How do I exclude sensitive fields with JSON.stringify?
Use a function replacer that returns undefined for keys to exclude: JSON.stringify(user, (key, val) => key === 'password' ? undefined : val, 2). For multiple fields, use a Set: (key, val) => sensitiveKeys.has(key) ? undefined : val. Alternatively, use an array replacer to whitelist only the keys you want to include.
What does JSON.stringify do with undefined, NaN, and Infinity?
NaN and Infinity are converted to null. Object properties whose value is undefined are dropped silently. undefined inside an array becomes null to preserve indices. Functions and Symbol-keyed properties are also dropped from objects. BigInt throws a TypeError. No warning is raised for any silent conversion — data just disappears.
What is the reviver argument in JSON.parse?
The reviver is the second argument to JSON.parse(text, reviver). It is called for every key-value pair in the parsed result, from the deepest nested values up to the root. Whatever it returns replaces that parsed value. The most common use is restoring Date objects from ISO 8601 strings, or reconstructing custom types like Map that JSON cannot represent natively.
Try it directly in your browser — free, no signup:
Open JSON Beautifier →