JSON.stringify() Complete Guide: indent, replacer, reviver, and edge cases

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 typeIn an object propertyIn an array
undefinedProperty omitted entirelyConverted to null
functionProperty omitted entirelyConverted to null
Symbol valueProperty omitted entirelyConverted to null
Symbol-keyed propertyAlways omittedN/A
NaNConverted to nullConverted to null
InfinityConverted to nullConverted to null
BigIntThrows TypeError: Do not know how to serialize a BigInt
DateCalls .toJSON() → ISO 8601 string (survives, but type is lost)
Map, SetConverted to {} — all data lost silently
Circular referenceThrows 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:

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 →
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds developer tools at JSON Dev Tools.