JSON.stringify Drops a Property — Why undefined, Functions, and Symbols Disappear

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:

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:

ValueAs an object propertyAs an array element
undefinedproperty omittednull
functionproperty omittednull
symbolproperty omittednull
nullkept as nullkept as null
NaN / Infinityvalue 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

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.

JSON Formatter JSON Diff All Error References
About the author

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