JSON.stringify undefined: Why Values Disappear and How to Handle It
JSON.stringify silently drops undefined values. Learn what gets removed, why it happens, and how to handle it safely in JavaScript and TypeScript.
Have broken JSON right now? Fix it free in under 1 second — no signup.
Fix My JSON →JSON.stringify(undefined) returns undefined — not the string "undefined", not null, not an error. Just undefined. And inside objects and arrays, the behavior differs: object properties with undefined values disappear entirely, while undefined in arrays becomes null. This silent behavior causes hard-to-debug bugs, especially when working with API responses, AI tool output, or data pipelines.
What JSON.stringify Does With undefined
// Standalone undefined
JSON.stringify(undefined) // → undefined (not a string)
// undefined in an object — property silently dropped
JSON.stringify({ a: 1, b: undefined, c: 3 })
// → '{"a":1,"c":3}'
// undefined in an array — becomes null
JSON.stringify([1, undefined, 3])
// → '[1,null,3]'
// Nested undefined
JSON.stringify({ user: { name: "Alice", email: undefined } })
// → '{"user":{"name":"Alice"}}'
This is defined in the JSON spec. undefined is not a valid JSON value, so serializers must either omit the key (objects) or substitute null (arrays).
Why This Causes Bugs
The silent drop is the problem. You don't get an error — the key just disappears. If you're sending this to an API or storing it in a database, the receiver gets a different shape than expected.
Common scenario: optional fields in API payloadsconst payload = {
userId: 123,
email: user.email, // undefined if user hasn't set one
phone: user.phone,
};
const body = JSON.stringify(payload);
// If email is undefined: '{"userId":123}' — both fields gone
The API receives an object missing required fields and returns a 400. The bug is invisible because console.log(payload) shows the full object before serialization.
How to Handle undefined in JSON.stringify
Replace undefined with null
const payload = {
userId: 123,
email: user.email ?? null,
phone: user.phone ?? null,
};
JSON.stringify(payload)
// → '{"userId":123,"email":null,"phone":null}'
Use the replacer argument
JSON.stringify accepts a replacer function as its second argument:
JSON.stringify(obj, (key, value) => {
if (value === undefined) return null;
return value;
});
// Example
const data = { a: 1, b: undefined, c: 3 };
JSON.stringify(data, (k, v) => v === undefined ? null : v);
// → '{"a":1,"b":null,"c":3}'
Explicit filter (when you want intentional dropping)
function stripUndefined(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== undefined)
);
}
JSON.stringify(stripUndefined({ a: 1, b: undefined, c: 3 }));
// → '{"a":1,"c":3}'
TypeScript: Preventing Serialization Issues at Compile Time
The problem with optional properties:interface User {
id: number;
email?: string; // can be undefined — TypeScript won't warn about JSON.stringify
}
Safer approach: use null for optional JSON values:interface UserPayload {
id: number;
email: string | null; // forces explicit conversion
}
const payload: UserPayload = {
id: 1,
email: user.email ?? null,
};
Type-safe JSON type:type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonObject | JsonArray;
type JsonObject = { [K in string]: JsonValue };
type JsonArray = JsonValue[];
function safeStringify(value: JsonValue): string {
return JSON.stringify(value);
}
This prevents passing objects with undefined values to the serializer at the type level.
Special Cases
Functions and symbols are also silently dropped:JSON.stringify({ fn: () => {}, sym: Symbol('x'), val: 42 })
// → '{"val":42}'
NaN and Infinity become null:JSON.stringify({ a: NaN, b: Infinity })
// → '{"a":null,"b":null}'
If you get unexpected null values in serialized output, NaN or Infinity from calculations is a common culprit.
class CustomDate {
constructor(public iso: string) {}
toJSON() { return this.iso; }
}
JSON.stringify({ created: new CustomDate("2026-05-19") })
// → '{"created":"2026-05-19"}'
undefined in AI-Generated Output
When an LLM outputs JavaScript-style objects (not proper JSON), it sometimes includes undefined for optional fields. If you parse this as JavaScript and re-stringify, the undefined values silently drop. More commonly, AI tools output NaN or Infinity for numerical edge cases.
undefined, NaN, Infinity, and other non-JSON values to null — before you parse the output. The API endpoint handles this automatically in production pipelines.
Quick Reference
| Input to stringify | Output |
|---|---|
undefined (standalone) | undefined (not a string) |
{ a: undefined } | {} (key dropped) |
[undefined] | [null] (index preserved) |
{ a: NaN } | {"a":null} |
{ a: Infinity } | {"a":null} |
{ a: () => {} } | {} (function dropped) |
{ a: Symbol() } | {} (symbol dropped) |
The fix is almost always the same: use null for absent values, use the replacer to convert undefined → null explicitly, and in TypeScript prefer string | null over string? for fields that will be serialized.
Circular References: The One Case That Throws
undefined is silently dropped, but circular references actually throw a TypeError:
const obj = { a: 1 };
obj.self = obj; // circular reference
JSON.stringify(obj);
// TypeError: Converting circular structure to JSON
This happens in practice with:
- Express
reqorresobjects (which have circular internal references) - DOM nodes stored in application state
- Objects that reference their parent container
function hasCircular(obj, seen = new WeakSet()) {
if (typeof obj !== 'object' || obj === null) return false;
if (seen.has(obj)) return true;
seen.add(obj);
return Object.values(obj).some(v => hasCircular(v, seen));
}
if (!hasCircular(data)) {
JSON.stringify(data);
}
Handle with a custom replacer:
function safeStringify(obj) {
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;
});
}
This replaces circular references with the string '[Circular]' rather than throwing. Useful for logging where you need output even if the object is structurally invalid.
Class Instances and Custom Serialization
Plain class instances don't serialize the way you might expect:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distanceTo(other) { / ... / }
}
const p = new Point(3, 4);
JSON.stringify(p);
// → '{"x":3,"y":4}' — only own enumerable properties, methods dropped
Methods are dropped (they're not enumerable properties). Prototype properties are also dropped. This is usually fine — but class instances with private fields or getter-based properties may serialize incorrectly:
class UserSession {
#token;
constructor(token, userId) {
this.#token = token; // private — NOT serialized
this.userId = userId;
}
get displayName() { return User-${this.userId}; } // getter — NOT serialized
}
const session = new UserSession('secret', 42);
JSON.stringify(session);
// → '{"userId":42}' — token and displayName both missing
Control serialization with toJSON():
class UserSession {
#token;
constructor(token, userId) {
this.#token = token;
this.userId = userId;
}
toJSON() {
return { userId: this.userId, hasToken: !!this.#token };
// Explicitly define what serializes — exclude the raw token
}
}
JSON.stringify(new UserSession('secret', 42));
// → '{"userId":42,"hasToken":true}'
toJSON() is called automatically by JSON.stringify. It lets you control the serialized shape without changing how the class works internally.
JSON.stringify and Date Objects
Date objects serialize to ISO strings, not to undefined — but this surprises many developers:
JSON.stringify({ created: new Date('2026-06-02') });
// → '{"created":"2026-06-02T00:00:00.000Z"}'
Date.prototype.toJSON() calls .toISOString() automatically. When you JSON.parse() the result, you get a string back, not a Date object:
const parsed = JSON.parse('{"created":"2026-06-02T00:00:00.000Z"}');
typeof parsed.created; // 'string', not Date
// If you need Date objects after round-tripping:
const revived = JSON.parse(json, (key, value) => {
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
Use the reviver argument to reconstruct Date objects after parsing.
Debugging: Finding Unexpected Missing Keys
When a key you expected disappears from serialized output, the fastest way to find why:
// 1. Log BEFORE stringify to see the actual value
console.log('email value:', typeof user.email, user.email);
// If it says "undefined undefined" — the key is undefined, not missing
// 2. Use JSON.stringify with null replacer to see ALL keys including undefined
// (they still won't appear, but this confirms the shape)
console.log(Object.keys(user)); // shows all own keys, even if value is undefined
// 3. Check where the value originates
// Optional chaining (?.) returns undefined if any part is missing
const email = response?.user?.contact?.email; // undefined if any step fails
A common culprit is optional chaining returning undefined silently:
const payload = {
email: response?.user?.email, // undefined if user or email is absent
};
// Fix: add a fallback
const payload = {
email: response?.user?.email ?? null,
};
React State and undefined Values
React's useState initializes with whatever you pass. If you serialize state that was never set, you get missing keys:
const [user, setUser] = useState(null); // safe — null serializes
const [profile, setProfile] = useState(); // undefined — serializes to nothing
// When sending to an API:
fetch('/api/update', {
method: 'POST',
body: JSON.stringify({ user, profile }),
// → '{"user":null}' — profile is silently dropped
});
Pattern: initialize with null, not undefined:
const [profile, setProfile] = useState(null); // null is valid JSON
Pattern: sanitize state before serializing:
function toApiPayload(state) {
return Object.fromEntries(
Object.entries(state)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, v ?? null])
);
}
FAQ
Why does JSON.stringify return undefined instead of throwing for undefined input?
The JSON spec defines undefined as not a valid JSON value. Rather than throw an error (which would break existing code that passes potentially-undefined values), the spec chose to return undefined to signal "no valid serialization exists." This is a design decision from the ECMAScript spec — consistent but easy to miss because undefined as a return value is easy to ignore.
How do I check if a value will be dropped before stringifying?
Check with value === undefined or typeof value === 'undefined' before including it in your object. For nested structures, use the replacer function to intercept and log every value: JSON.stringify(obj, (k, v) => { if (v === undefined) console.log('dropping:', k); return v; }).
Does undefined get dropped in JSON.parse output too?
No — JSON.parse only parses valid JSON, and undefined is not valid JSON. So parsed output will never contain undefined values. The undefined issue only occurs on the serialization side with JSON.stringify.
What's the difference between null and undefined in JSON?
null is a valid JSON value. undefined is not. In serialized JSON, null appears as the literal null. There is no way to represent undefined in JSON — it either gets dropped (object properties) or converted to null (array elements). This is why TypeScript's string | null is safer than string | undefined for values that will be serialized.
Can I make JSON.stringify throw on undefined values instead of dropping them?
Yes — use a replacer that throws when it encounters undefined:
JSON.stringify(obj, (key, value) => {
if (value === undefined) throw new Error(undefined value at key: "${key}");
return value;
});
This is useful in tests or validation layers where silent drops would cause data corruption downstream.
Still dealing with broken JSON?
Paste it in and get it fixed in under 1 second — free, no signup, no install. Works with ChatGPT, Claude, n8n, and any AI output.
Fix My JSON Free →Related Articles