JSON.stringify accepts a second argument called the replacer. What are its two valid forms, and what does each one do to the output?
answer
- two shapes: list of keys, or a callback
- the list is an allowlist, not a blocklist
- it also decides key order
- the callback fires once for the root
- return undefined to drop a property
basics
~20 sThe replacer is JSON.stringify's second argument. An array of keys acts as an allowlist that also fixes the output key order; a function is called for every key and value pair and returns the value to serialize, or undefined to omit that property.
solid answer
~50 sThe replacer takes either an array or a function. In the **array** form you pass the property names you want kept — `JSON.stringify(user, ['name', 'id'])` — and it acts as an allowlist applied at every level of nesting, with the output keys following the array's order rather than the object's. It filters object properties only; array elements are never filtered by it. In the **function** form your callback is invoked with `(key, value)` for every entry, with `this` bound to the object holding that value. Whatever you return is what gets serialized, and returning `undefined` omits an object property or turns an array element into `null`. The very first call is special: the key is the empty string and the value is the whole top-level value, so a callback that forgets to return that value produces `undefined` for the entire result.
code
javascript · 15 linesconst user = { id: 7, name: 'Ada', password: 'hunter2', role: 'admin' };
// Array form: allowlist, and the array's order wins.
console.log(JSON.stringify(user, ['name', 'id']));
// {"name":"Ada","id":7}
// Function form: conditional redaction.
console.log(JSON.stringify(user, (key, value) =>
key === 'password' ? undefined : value
));
// {"id":7,"name":"Ada","role":"admin"}
// The first call is key '' with the whole value - forget it and you get nothing.
console.log(JSON.stringify(user, (key, value) => (key === 'name' ? value : undefined)));
// undefinedgo deeper
Know that a second argument exists and that passing an array of key names limits the output to those keys. Be able to name the redaction use case even if you have not written one.
Explain both forms precisely: the array as a deep allowlist that also fixes key order, and the function's (key, value) signature, its this binding, undefined meaning omit, and the empty-string call for the root.
Show the production judgment: redaction by key name is fragile, a replacer runs on every node so it must be cheap, and substituting a placeholder changes the value's type for schema-validating consumers.
Own the boundary decision — whether payload shaping belongs at scattered stringify calls at all, versus an explicit serialization layer with a declared output contract that the replacer merely enforces.
## Why a replacer exists Serialization defaults are rarely exactly what you want to send. You may need to strip credentials, redact fields, shrink a payload, convert a type the format cannot express, or force a stable key order. The replacer is the single official hook for all of that at the `JSON.stringify` call site, and it comes in two shapes with quite different semantics. ## Form one: an array of property names ```js const user = { id: 7, name: 'Ada', password: 'hunter2', role: 'admin' }; JSON.stringify(user, ['name', 'id']); // '{"name":"Ada","id":7}' ``` Three things are worth noticing. **It is an allowlist, not a denylist.** Anything not named is gone. That makes it a safe default for "emit exactly these fields", and a poor tool for "emit everything except one field", because a newly added property is excluded by default. **It sets the output order.** The keys appear in the array's order, not the object's insertion order. That is the simplest way to get a deterministic key order out of serialization, which matters when you hash or diff the text. **It applies at every depth, and only to objects.** A name in the list is kept wherever it occurs in the tree, which surprises people with nested structures that reuse key names. Array elements are never filtered: `JSON.stringify(['x', 'y'], ['0'])` still gives `'["x","y"]'`, because the filter is defined over object properties, not indices. Entries that are neither strings nor numbers (nor String/Number objects) are ignored, and duplicates collapse. ## Form two: a function ```js JSON.stringify(user, (key, value) => (key === 'password' ? undefined : value)); // '{"id":7,"name":"Ada","role":"admin"}' ``` The callback receives the property key and its value, and `this` is bound to the object that holds the property — which is how you inspect a sibling field while deciding what to do with the current one, and the reason a non-arrow function is sometimes required. The return value is what gets serialized in place of the original. Return `undefined` and the property is omitted from an object, or becomes `null` inside an array — the same positional rule that governs unserializable values generally. ## The empty-string first call Before visiting anything, the algorithm wraps your value in a holder object and calls the replacer once with key `''` and the whole value: ```js JSON.stringify({ a: 1 }, function (key, value) { console.log(JSON.stringify(key)); // '""' then '"a"' return value; }); ``` This catches people writing a filter that returns something only for keys they recognise: ```js JSON.stringify({ a: 1, b: 2 }, (key, value) => (key === 'a' ? value : undefined)); // undefined -- the root itself was filtered out on the '' call ``` The root was rejected first, so nothing was ever traversed. Any function replacer must pass the root through. ## Traversal order and recursion The function form is applied **top-down**: the replacer sees a container before its children, and whatever it returns for the container is what gets walked. Returning a different object from the replacer therefore redirects the traversal into the substitute, and the replacer is then applied to that substitute's properties too. That is what makes type conversion work: ```js JSON.stringify({ ids: new Set([1, 2]) }, (key, value) => value instanceof Set ? [...value] : value ); // '{"ids":[1,2]}' ``` It is also how you can accidentally build an infinite recursion, by returning a fresh object that contains a value of the same shape on every visit. One ordering detail matters when a value defines its own serialization hook: that hook runs first, so by the time your replacer sees the value it may already have been converted to whatever the hook produced, not the original object. ## Choosing between the forms - Fixed, known output shape and a need for stable key order: the **array** form. It is declarative and cannot accidentally leak a new field. - Conditional logic, redaction, type conversion, cycle tracking, or anything that needs to look at the value: the **function** form. - Both at once is not possible; you pass one argument. If you need allowlisting *and* conversion, express the allowlist inside the function. ## Practical cautions The replacer runs for every node in the tree, so keep it cheap — a regular expression compiled inside the callback is compiled on every property. Redaction by key name is fragile if the same secret also appears under a different key or nested inside a string. And a replacer that emits a placeholder such as `'[Redacted]'` changes the value's *type* as well as its content, which downstream consumers with a schema may reject.
- Why does a function replacer that only returns values for the keys it recognises produce undefined?Because the first invocation is not a property at all: the algorithm wraps the value in a holder and calls the replacer with key `''` and the entire top-level value. A callback that returns `undefined` for unrecognised keys rejects the root, so traversal never begins and the whole call returns `undefined`. Always pass the empty-string key through unchanged.
- Can you use the array form to exclude a single sensitive field?Not directly — the array form is an allowlist, so you would have to enumerate every field you want kept and keep that list in sync as the object grows. A newly added field would be silently dropped, and a renamed one silently lost. Exclusion is the function form's job: return `undefined` for the key you want gone and pass everything else through.
- Why is a non-arrow replacer sometimes necessary?Because the replacer is called with `this` bound to the object that holds the current property, and an arrow function has no own `this` binding to receive it. If your decision depends on a sibling field — redact `value` only when `this.type === 'secret'` — you need a regular function expression. If you only ever look at the key and value, an arrow is fine.
- How do you get a deterministic key order out of JSON.stringify?Pass the array form with the keys listed in the order you want; the output follows the array, not the object's insertion order. That is the built-in way to make the text stable for hashing or diffing. A function replacer cannot reorder keys, so the alternative is to rebuild the object with sorted keys before serializing.
saying these in an interview costs you the question
- Calls the array form a blocklist of keys to exclude
- Forgetting the replacer is also called once for the root
- Expects the array form to filter array elements too
- Thinks returning undefined for an array element omits it
- Uses an arrow function then expects this to be the holder object