JSON.stringify() with replacer

ES5+

The replacer parameter of JSON.stringify() intercepts every key-value pair during serialization: return a different value to transform it, or undefined to omit the property entirely. Passing an array of property names instead acts as a whitelist of properties to keep at every level.

Syntax

JSON.stringify(value, replacer)

Parameters

value any

The value to convert to a JSON string

replacer Function

A function that receives key and value, and returns the replacement value

Return Value

string

A JSON string with transformed values

Examples

JavaScript
const obj = {
  name: 'John',
  password: 'secret123',
  age: 30
};
const result = JSON.stringify(obj, (key, value) => {
  if (key === 'password') return undefined;
  return value;
});
console.log(result);
Output:
// '{"name":"John","age":30}'

📌 When to Use

Use a replacer function when the serialized form must differ from the in-memory form and you cannot, or should not, modify the objects themselves. The bread-and-butter cases: redacting secrets before objects reach logs or third-party APIs; converting types JSON cannot carry — BigInt to string, Map and Set to arrays, Error objects to { message, stack } records; pruning heavy or irrelevant fields like cached DOM references and internal bookkeeping; and breaking circular references by tracking visited objects in a WeakSet and returning a placeholder on the second encounter. The array form is a separate, simpler tool: JSON.stringify(user, ["id", "name"]) whitelists exactly the listed property names at every nesting level, which is ideal for emitting a stable public subset of a richer object. Choose a replacer over pre-processing when the transformation is serialization-specific — the object stays untouched for the rest of the program. Prefer defining toJSON() on a class instead when the transformation is intrinsic to the type and should apply everywhere it is ever serialized, not just at the call sites that remember to pass the replacer.

⚠️ Common Mistakes

Forgetting the root call - the replacer is first invoked with an empty-string key and the entire root value. A replacer that switches on key names usually falls through correctly, but one that transforms by value type (say, every object) can mangle the whole result if it does not account for that initial call.

Expecting the array form to filter selectively per level - the whitelist applies to every object at every depth, so ["name"] keeps the name property of the root and of every nested object, while array elements are unaffected. Fine-grained per-level filtering needs a function replacer.

Testing values before realizing toJSON() has already run - the value argument arrives after toJSON conversion, so checks like value instanceof Date always fail because the replacer sees the ISO string. Inspect this[key] inside a non-arrow replacer when you need the pre-conversion object.

Returning undefined from the replacer removes the property entirely, which may not be the intended behavior.

✅ Best Practices

Handle circular structures with a WeakSet: keep a set of already-seen objects, return "[Circular]" when one repeats, and you have a crash-proof logger in six lines - the exact trick behind most safe-stringify utilities on npm.

Match sensitive keys by pattern, not by exact list - a regex like /password|token|secret|authorization|cookie/i catches apiToken, refreshToken, and friends that a hardcoded array misses; redaction that depends on remembering to update a list eventually leaks something.

Keep replacers pure and fast - they run once per key over the whole tree, so no network calls, no logging inside the replacer, and no mutation of the objects being serialized; side effects that fire during serialization are miserable to debug.

Use a replacer function for complex transformations and a replacer array for simple property filtering.

⚡ Performance Notes

A function replacer is invoked for every key of every object and array element in the tree, plus the synthetic root call, so its cost multiplies by total node count: a 10,000-node state tree pays 10,000 extra function calls plus whatever the body does. Cheap typeof checks are fine; per-call regex construction is not — hoist patterns out of the replacer and precompile them once, and prefer key-name switches over expensive value inspection. If only a couple of known fields need transformation, transforming a shallow copy of the object before a plain stringify is usually faster and clearer than a whole-tree replacer. The array (whitelist) form is handled natively and costs close to nothing beyond the filtering itself. For redaction over huge log payloads, sample or truncate first — a replacer cannot make an oversized serialization cheap, it can only make it slower.

🌍 Real World Example

Logging Sanitizer

This log sanitizer solves the two problems that make raw object logging dangerous: secret leakage and unbounded size. A precompiled case-insensitive pattern redacts any key resembling credentials — password, secret, token, key, auth — while a length check truncates strings past 100 characters with an explicit "[truncated]" marker, keeping log lines readable and cheap to store. Because the filtering happens inside the replacer, the original request object is untouched and remains fully usable after logging, which pre-processing approaches cannot promise.

function sanitizeForLogging(obj) {
  const sensitivePatterns = /password|secret|token|key|auth/i;
  const maxStringLength = 100;

  const replacer = (key, value) => {
    if (sensitivePatterns.test(key)) {
      return '[HIDDEN]';
    }
    if (typeof value === 'string' && value.length > maxStringLength) {
      return value.substring(0, maxStringLength) + '... [truncated]';
    }
    return value;
  };

  return JSON.stringify(obj, replacer, 2);
}

const request = {
  url: '/api/users',
  body: { password: 'secret123' },
  longField: 'a'.repeat(200)
};

console.log(sanitizeForLogging(request));

Related Methods