Quick answer
JSON.stringify silently omits any object property whose value is undefined, a function, or a symbol — because JSON has no type for them. The same values become null inside an array. It is spec behaviour, not a bug. Convert the value (to null, a string, or a plain object) with a replacer before stringifying.
The symptom: a key that vanishes
There is no thrown error here — that is what makes this so confusing. The code runs fine, but a property you set is simply not in the output:
const user = {
name: 'Ada',
age: undefined,
greet: function () { return 'hi'; },
id: Symbol('uid'),
};
JSON.stringify(user);
// => '{"name":"Ada"}'
// age, greet, and id are all gone
Three of the four properties disappeared. No warning, no exception — JSON.stringify just left them out. Once you know the rule, the output is completely predictable.
Why JSON.stringify drops these values
JSON is a data format with a small, fixed set of types: string, number, boolean, null, object, and array. JavaScript has types that JSON cannot represent — undefined, functions, and symbols chief among them. When JSON.stringify meets a value it cannot map to a JSON type, it does not guess and it does not throw; it follows the specification:
undefined— no JSON equivalent (JSON only hasnull).- functions — JSON carries data, not executable code.
- symbols — a JavaScript-only primitive with no serialized form.
This is intentional and stable across every JavaScript engine. The trap is not the rule itself but the fact that the same value behaves differently depending on where it sits.
The array-vs-object difference (the part everyone misses)
This is the single most important thing to understand, and most explanations skip it. An unrepresentable value is treated differently depending on whether it is an object property or an array element:
| Value | As an object property | As an array element |
|---|---|---|
undefined | property omitted | null |
| function | property omitted | null |
| symbol | property omitted | null |
null | kept as null | kept as null |
NaN / Infinity | value becomes null (kept) | null |
JSON.stringify({ a: undefined }); // => '{}' (key removed)
JSON.stringify([undefined]); // => '[null]' (index preserved)
JSON.stringify({ a: () => 1 }); // => '{}'
JSON.stringify([() => 1]); // => '[null]'
The reason for the asymmetry: an array's meaning depends on its length and index positions. Dropping element 2 would shift every later element down and silently corrupt the data, so JSON.stringify substitutes null to hold the slot. An object has no positional contract, so the key is simply removed.
The top-level case: stringify can return undefined
If the value you pass is itself one of the dropped types, JSON.stringify does not return a string at all — it returns the JavaScript value undefined:
JSON.stringify(undefined); // => undefined (not "undefined")
JSON.stringify(function () {}); // => undefined
JSON.stringify(Symbol('x')); // => undefined
// This bites when you write the result straight to disk or a response:
fs.writeFileSync('out.json', JSON.stringify(maybeUndefined));
// If maybeUndefined is undefined, you write the literal text "undefined"
// — an invalid JSON file that later throws when parsed.
That invalid file is a common root cause of a later Expecting value (Python) or Unexpected token u in JSON (JavaScript) error when something tries to read it back. Always guard the return value before persisting or sending it.
Fix 1: use a replacer to substitute the value
The second argument to JSON.stringify is a replacer function. It runs for every key and lets you return a JSON-safe value in place of one that would be dropped:
const data = { name: 'Ada', age: undefined, role: undefined };
const json = JSON.stringify(data, (key, value) =>
value === undefined ? null : value
);
// => '{"name":"Ada","age":null,"role":null}'
Note that the replacer cannot see the original undefined for a key that JavaScript has already resolved in some engines for nested values — but for the common case above it works cleanly, turning every undefined into an explicit null you can rely on.
Fix 2: build a serializable shape first (recommended)
The most robust fix is to stop depending on JSON.stringify's quirks entirely and construct a plain object that contains only JSON-safe values. This makes the output explicit and self-documenting:
function toDTO(user) {
return {
name: user.name,
age: user.age ?? null, // undefined -> null on purpose
role: user.role ?? 'guest', // a real default
// functions and symbols are simply never included
};
}
JSON.stringify(toDTO(user)); // every key is intentional
This is the same discipline as a serializer or DTO layer: decide what the wire format should contain, rather than letting the engine decide by omission.
Fix 3: serialize a function only if you really mean to
Occasionally people actually want the function's source text (for a config or a code-transport format). JSON cannot hold a function, but you can store its source as a string — with the strong caveat that re-creating a function from a string (eval / new Function) is a security risk and almost never the right design:
const obj = { handler: function () { return 42; } };
JSON.stringify(obj, (k, v) =>
typeof v === 'function' ? v.toString() : v
);
// => '{"handler":"function () { return 42; }"}'
In the vast majority of cases the right answer is the opposite: you are trying to send data, and a function leaked into the object by accident. Removing it (Fix 2) is the real fix.
Why Date works but Map and Set come out empty
Two more surprises trip people up once they understand the basic rule. First, a Date serializes perfectly — but a Map or Set turns into {}:
JSON.stringify({ when: new Date() });
// => '{"when":"2026-06-25T10:00:00.000Z"}' ✅ works
JSON.stringify({ ids: new Map([['a', 1]]) });
// => '{"ids":{}}' ⚠️ entries gone, not an error
JSON.stringify({ tags: new Set([1, 2, 3]) });
// => '{"tags":{}}' ⚠️ values gone
Date works because it has a built-in toJSON() method — if a value defines toJSON(), JSON.stringify calls it and serializes the return value. Map and Set have no toJSON() and no enumerable own properties, so they stringify as an empty object: the data is silently lost, with no error. Convert them explicitly first:
JSON.stringify({ ids: Object.fromEntries(map) }); // Map -> object
JSON.stringify({ tags: [...set] }); // Set -> array
// Or give your own class a toJSON() to control its wire form:
class Money {
constructor(cents) { this.cents = cents; }
toJSON() { return (this.cents / 100).toFixed(2); }
}
JSON.stringify({ price: new Money(1999) }); // => '{"price":"19.99"}'
The replacer array: an allowlist of keys
The second argument can also be an array of strings instead of a function. In that form it is a key allowlist — only those keys survive, everything else is dropped. This is handy for stripping internal or sensitive fields, but it is a frequent source of "my property disappeared":
const user = { id: 1, name: 'Ada', password: 'secret', token: 'xyz' };
JSON.stringify(user, ['id', 'name']);
// => '{"id":1,"name":"Ada"}' — password and token intentionally dropped
// Gotcha: the allowlist applies at EVERY level. A key you forgot to list
// in a nested object vanishes too — check the array if nested data is missing.
If a property is missing and you are passing an array as the second argument, that array — not the type rules — is almost certainly the cause.
Compare before and after serialization
The fastest way to catch a dropped key is to look at the input object and the parsed output side by side. Paste both into the JSON Diff tool to see exactly which keys were removed or turned to null during the round trip.
Debugging checklist
- ✓ Is the missing value
undefined, a function, or a symbol? Those are dropped from objects. - ✓ Is it inside an array? Then it became
null, not removed — check for unexpectednullholes. - ✓ Did
JSON.stringifyreturnundefined? The top-level value was itself unrepresentable. - ✓ Want the key present? Map the value to
nullor a string with a replacer. - ✓ Want full control? Build a plain serializable object (DTO) before stringifying.
- ✓ Seeing
nullwhere a number should be? Check forNaN/Infinity— same type system, value coerced tonull. - ✓ Property is an empty
{}? It was aMaporSet— convert withObject.fromEntries/ spread first. - ✓ Passing an array as the 2nd argument? That is a key allowlist — unlisted keys are dropped at every level.
- ✓ Reading the result back fails? A written
undefinedproduced an invalid JSON file.
Frequently Asked Questions
Why does JSON.stringify drop a property?
JSON.stringify omits any object property whose value is undefined, a function, or a symbol, because JSON has no type to represent them. It is not a bug — it is the JSON specification's type system. The same values become null when they appear as array elements instead of object properties.
What is the difference between how JSON.stringify treats undefined in an object versus an array?
In an object, an undefined, function, or symbol value causes the whole property to be omitted from the output. In an array, the same value is replaced by null so that array indexes are preserved. So {a: undefined} becomes {} but [undefined] becomes [null].
Why does JSON.stringify(undefined) return undefined and not a string?
When you pass undefined, a function, or a symbol as the top-level value, JSON.stringify returns the JavaScript value undefined — not the string 'undefined' and not '"undefined"'. There is no valid JSON document for those types, so nothing is produced. Always guard against an undefined return before writing or sending the result.
How do I keep an undefined or function property in JSON output?
Convert the value to something JSON can represent before stringifying. Use a replacer function as the second argument to JSON.stringify to substitute a value (for example null or a string), or build a plain serializable object first. For functions, serialize their source with toString() only if you truly need it — usually you should send data, not code.
Does JSON.stringify drop null values too?
No. null is a valid JSON value, so a property set to null is kept: {a: null} serializes to {"a":null}. Only undefined, functions, and symbols are dropped from objects. If you want missing data represented in the output, use null instead of undefined.
Why is my NaN or Infinity showing up as null?
JSON has no representation for NaN, Infinity, or -Infinity, so JSON.stringify converts them to null rather than dropping the property. This is a related quirk of the same type system: the property stays, but its value becomes null. Replace these numbers with a sentinel of your choice in a replacer if null is not acceptable. See NaN / Infinity not allowed in JSON.
See exactly what your object serializes to
Paste your JSON.stringify output into the formatter to spot missing keys and stray null holes — nothing is uploaded to a server.