JSON.stringify() Replacer and Space: Filter, Transform, and Pretty-Print JSON

JSON.stringify(value, replacer, space) accepts three arguments. Most developers only use the first. The second argument — replacer — is a function or array that filters and transforms keys before serialization. The third argument — space — sets the indentation: a number (0–10 spaces) or a string (e.g. '\t' for tabs). Together, these two optional arguments cover the most common JSON serialization needs: excluding sensitive fields, converting non-serializable values, and producing readable output. The replacer argument is called recursively for every key-value pair in the structure, giving you full control over what appears in the final JSON string. Need to format JSON right now? Jsonic's JSON Formatter does it instantly in the browser.

Format and pretty-print JSON instantly — no setup required.

Open JSON Formatter

The space argument: pretty-printing JSON

The third argument to JSON.stringify controls indentation in the output string. Pass a number between 1 and 10 to indent with that many spaces, or pass a string (like '\t') to use that string as the indent unit. Values above 10 are clamped to 10; strings longer than 10 characters are truncated to 10. Passing0, an empty string, or omitting the argument entirely produces compact (minified) output with no added whitespace — useful when transmitting JSON over the network to minimize payload size.

The two most common choices are 2 (2-space indent, default in many JavaScript formatters and linters) and '\t' (tab indent, preferred in some codebases for alignment). The space argument only affects readability — it does not change which keys are included or how values are represented. This is identical to what Jsonic's JSON Formatter does when you click "Format": it calls the equivalent of JSON.stringify(parsed, null, 2)under the hood.

const obj = { name: 'Alice', age: 30, roles: ['admin', 'user'] }

// Compact (minified) — omit space or pass 0
JSON.stringify(obj)
// '{"name":"Alice","age":30,"roles":["admin","user"]}'

// 2-space indent
JSON.stringify(obj, null, 2)
// {
//   "name": "Alice",
//   "age": 30,
//   "roles": [
//     "admin",
//     "user"
//   ]
// }

// Tab indent
JSON.stringify(obj, null, '	')
// {
// 	"name": "Alice",
// 	"age": 30,
// 	"roles": [
// 		"admin",
// 		"user"
// 	]
// }

Use the JSON Minifier to strip whitespace from pretty-printed JSON before sending it in an API response. For config files checked into source control, 2-space indentation is the dominant convention across the JavaScript ecosystem (ESLint, Prettier, npm's package.json).

Replacer as an array: allowlist specific keys

Passing an array of strings as the replacer tells JSON.stringify to include only the keys listed in that array. Every other key is excluded from the output. This filtering applies recursively — nested objects are also filtered, so any key not in the array is excluded at all nesting levels. This is the safest way to serialize API responses: you explicitly declare which fields are safe to expose (allowlist), rather than trying to remember which fields to block (denylist). If you add a new field to an object later, it won't accidentally appear in the output.

const user = {
  id: 1,
  name: 'Alice',
  password: 'secret',
  email: 'alice@example.com',
}

JSON.stringify(user, ['id', 'name', 'email'], 2)
// {
//   "id": 1,
//   "name": "Alice",
//   "email": "alice@example.com"
// }
// password is excluded — not in the allowlist

The array replacer also works on nested objects, but only keys that appear in the array survive at every level. If a nested object has a key street andstreet is not in your array, it will be excluded even if the parent key address is included. This means you need to list every key you want to keep, including deeply nested ones. For finer-grained control over nested structures — for example, including a key in one nested object but not another — use the function replacer instead.

Replacer as a function: transform values

A function replacer is called with (key, value) for every key-value pair encountered during serialization. Whatever the function returns is used as the serialized value. Return undefined to omit the key from output entirely. Return any other value — including a transformed version of the original — to include it. The replacer is called recursively through the entire object tree.

The first call is special: the key is an empty string "" and the value is the root object itself. Returning undefined from this first call serializes nothing (produces undefined, not "undefined"). This is useful for conditional serialization — check a flag before returning the root value to control whether anything is serialized at all.

const obj = {
  name: 'Alice',
  password: 'secret',
  salary: 95000,
  createdAt: new Date('2024-01-15'),
}

JSON.stringify(obj, (key, value) => {
  if (key === 'password') return undefined         // exclude
  if (key === 'salary') return '[redacted]'        // transform
  if (value instanceof Date) return value.toISOString()  // convert
  return value                                     // keep as-is
}, 2)
/*
{
  "name": "Alice",
  "salary": "[redacted]",
  "createdAt": "2024-01-15T00:00:00.000Z"
}
*/

Common use cases: masking PII fields before logging, converting Date objects to a custom format, coercing non-serializable values to strings, and stripping internal metadata keys that start with _. For a complete introduction to serialization fundamentals, see the JSON.stringify tutorial.

Exclude undefined, functions, and symbols

JSON.stringify silently drops three value types when they appear as object property values: undefined, function, and symbol. The key and value are removed from the output without any error or warning. In arrays, however, undefined and functions become null to preserve array length and index positions. Symbols are also dropped from arrays entirely. This asymmetric behavior can cause subtle bugs if you rely on array indexes after serialization.

// In objects: undefined/function/symbol keys are silently dropped
JSON.stringify({ a: 1, b: undefined, c: () => {}, d: Symbol('x') })
// '{"a":1}'

// In arrays: undefined and functions become null (preserves index)
JSON.stringify([1, undefined, () => {}, 3])
// '[1,null,null,3]'

Use the replacer function to handle these before they are silently dropped. For example, convert undefined to null to make the key explicit in the output, or convert functions to their .toString() for debugging purposes. This makes the serialization behavior transparent rather than relying on silent omission that can mask data loss.

// Convert undefined to null explicitly
JSON.stringify(
  { a: 1, b: undefined },
  (key, value) => value === undefined ? null : value
)
// '{"a":1,"b":null}'

toJSON() method: custom serialization

If an object has a toJSON() method, JSON.stringify calls it automatically and uses the return value in place of the object. This gives any class full control over its own serialization without requiring a replacer function at the call site. The Date built-in uses this mechanism internally — that's why dates serialize as ISO 8601 strings automatically. You can define toJSON() on any class or plain object.

class Money {
  constructor(amount, currency) {
    this.amount = amount
    this.currency = currency
  }
  toJSON() {
    return `${this.amount} ${this.currency}`
  }
}

JSON.stringify({ price: new Money(9.99, 'USD') })
// '{"price":"9.99 USD"}'

toJSON() is called before the replacer function runs. This means the replacer sees the already-transformed value — not the original object instance. If you need to intercept the raw object before toJSON() has run, you cannot do that from within a replacer; instead, override toJSON() on the class itself. When designing classes that will be serialized frequently, prefertoJSON() over call-site replacers — it keeps the serialization logic co-located with the class definition.

Handle circular references

JSON.stringify throws TypeError: Converting circular structure to JSON when it encounters an object that references itself directly or indirectly. This is a common source of crashes in code that serializes complex graphs, linked list nodes, or DOM-adjacent objects. The solution is to use a replacer that tracks already-seen objects with a WeakSet and replaces any repeat reference with a placeholder. For more context and approaches, see the guide on circular reference errors.

function safeStringify(obj, indent = 0) {
  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
  }, indent)
}

const a = { name: 'a' }
a.self = a  // circular reference
safeStringify(a, 2)
// '{
  "name": "a",
  "self": "[Circular]"
}'

The WeakSet is the right data structure here because it holds object references weakly, allowing garbage collection of objects that are no longer reachable. Using a regular Set would prevent GC for the lifetime of the safeStringify call. Note that the seen set is per-call — a new set is created each time safeStringify is invoked, so there is no cross-call state leakage. This pattern is also useful for deep clone objects that may contain cycles.

Serialize BigInt values

JSON.stringify throws TypeError: Do not know how to serialize a BigInt by default when it encounters a BigInt value. JavaScript's built-in JSON specification does not include BigInt because IEEE 754 double-precision numbers cannot represent all 64-bit integers without precision loss. The safest workaround is a replacer that converts BigInt to string before serialization.

JSON.stringify(
  { id: 9007199254740993n, name: 'Alice' },
  (key, value) => typeof value === 'bigint' ? value.toString() : value
)
// '{"id":"9007199254740993","name":"Alice"}'

Always serialize BigInt as a string, not as a number. Any integer larger than Number.MAX_SAFE_INTEGER (253 - 1 = 9,007,199,254,740,991) cannot be represented exactly as a JSON number in JavaScript — the value will be silently rounded when parsed. Serialize as "id": "9007199254740993", not "id": 9007199254740993. On the receiving end, use BigInt(jsonObj.id) to convert back. If you control both sides of the API, consider using a dedicated BigInt JSON library like json-bigint. For parsing such values on the receiving end, see JSON.parse().

Performance: avoid repeated JSON.stringify calls

If you call JSON.stringifyrepeatedly with the same replacer function, define the replacer outside the loop. Creating a new function inside a loop has minor overhead from function object allocation and closure capture, and it prevents V8 from optimizing the inner function across iterations. For most applications this is negligible, but at high frequency (>100k/s) it adds up.

// Bad: new function created on every iteration
for (const item of items) {
  const json = JSON.stringify(item, (k, v) => k === 'secret' ? undefined : v)
}

// Good: replacer defined once and reused
const replacer = (k, v) => k === 'secret' ? undefined : v
for (const item of items) {
  const json = JSON.stringify(item, replacer)
}

For high-frequency serialization, consider alternatives. fast-json-stringify uses a JSON Schema to generate a dedicated serializer function, running 2–5× faster than the built-in by skipping runtime reflection. flatted handles circular references efficiently with a two-pass format. Benchmarks: native JSON.stringify handles roughly 1–2 million small objects per second in V8; fast-json-stringify handles 3–10 million with a predefined schema. Profile your actual workload before optimizing — the built-in is almost always fast enough for typical web server request handlers.

Frequently asked questions

What does the replacer argument do in JSON.stringify?

The replacer is the second argument to JSON.stringify. It controls which keys are included in the output and how values are transformed. It can be either an array of strings or a function. An array replacer acts as an allowlist: only keys listed in the array appear in the output, at any nesting depth. A function replacer is called for every key-value pair in the object tree. It receives the key as a string and the current value. Whatever the function returns becomes the serialized value for that key. Returning undefined excludes the key entirely. Returning a transformed value (e.g., converting a Date to a formatted string, or masking a password field) replaces the original. The replacer is called recursively, starting with the root value (key is an empty string ""). Passing null as the replacer is equivalent to passing no replacer — all keys are included.

How do I pretty-print JSON with JSON.stringify?

Pass a number or string as the third argument (space). JSON.stringify(obj, null, 2) produces output indented with 2 spaces. JSON.stringify(obj, null, 4) uses 4 spaces. JSON.stringify(obj, null, '\t') uses tab characters. Numbers are clamped to 10; strings are truncated to 10 characters. Passing 0, an empty string, or omitting the argument produces compact (minified) output with no whitespace. The space argument only affects readability — it does not change the data structure. When sharing JSON between systems, always use compact format to minimize payload size; use the space argument only for logging, debugging, or human-readable config files. Jsonic's JSON Formatter uses 2-space indentation by default, matching the most common convention in the JavaScript ecosystem.

How do I exclude fields from JSON.stringify?

Two approaches: (1) Array replacer — pass an array of key names to include: JSON.stringify(user, ['id', 'name']) includes only id and name, excluding all other fields. Nested objects are also filtered. (2) Function replacer — return undefined for keys you want to exclude: JSON.stringify(user, (k, v) => k === 'password' ? undefined : v). The array approach is safer for API serialization because you explicitly list what to expose (allowlist), rather than blocking specific fields (denylist). Adding a new field to the object won't accidentally include it. The function approach is more flexible — it lets you exclude based on value type, key patterns, or conditional logic. Combine both approaches by using a function that checks against a set of allowed keys: const allowed = new Set(['id', 'name']); JSON.stringify(user, (k, v) => k === '' || allowed.has(k) ? v : undefined).

How do I serialize Date objects with JSON.stringify?

Date objects have a built-in toJSON() method that serializes them as ISO 8601 strings (e.g., "2024-01-15T10:30:00.000Z"). This happens automatically: JSON.stringify({ date: new Date() }) produces '{"date":"2024-01-15T10:30:00.000Z"}'. To customize date serialization — for example, outputting Unix timestamps instead of ISO strings — use a replacer function: JSON.stringify(obj, (k, v) => v instanceof Date ? v.getTime() : v). Note that inside the replacer, Date values have already been converted to ISO strings by toJSON() before the replacer runs. To intercept the raw Date object, you need to check the original value before stringify calls toJSON(). One approach: wrap dates in a class with a custom toJSON() that preserves the Date for downstream access.

How do I handle BigInt in JSON.stringify?

JSON.stringify throws TypeError: Do not know how to serialize a BigInt when it encounters a BigInt value. JavaScript's built-in JSON does not support BigInt natively because IEEE 754 numbers cannot represent all 64-bit integers without loss of precision. The safest workaround is a replacer that converts BigInt to string: JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v). Always serialize BigInt as a string, not as a number — deserializing a large integer as a JSON number will lose precision in any JavaScript JSON parser. Serialize as: "id": "9007199254740993" not "id": 9007199254740993. On the receiving end, use BigInt(jsonObj.id) to convert back. If you control both sides of the API, consider using a dedicated BigInt JSON library like json-bigint.

What is the difference between JSON.stringify replacer and toJSON()?

toJSON() and the replacer function are both serialization hooks, but they operate at different levels. toJSON() is a method on an individual object — it controls how that specific object serializes itself. It is called automatically by JSON.stringify before the replacer runs. The replacer is a global transform applied to every key-value pair in the entire structure. The execution order is: toJSON() runs first (on each object that has one), producing a transformed value; then the replacer receives that transformed value. This means you cannot use the replacer to intercept the pre-toJSON() value — the replacer sees the result after toJSON() has run. Use toJSON() for class-level serialization logic that belongs with the class; use the replacer for application-level transforms (filtering sensitive fields, format conversions) that are applied at serialization time regardless of the object type.

Ready to work with JSON.stringify?

Use Jsonic to format and pretty-print your JSON instantly in the browser. You can also minify JSON to compact output for API payloads, or explore JSON.parse() for the reverse operation.

Open JSON Formatter