JSON undefined Is Not Valid — Why Keys Silently Disappear and How to Stop It

Quick answer

💡undefined is a JavaScript-specific value absent from the JSON specification. JSON.stringify silently drops object keys whose values are undefined, replacing them with nothing — no key, no null, no error. Replace undefined with null when the field must be present, use the nullish coalescing operator (??) at assignment time, and validate your payload shape before sending it to catch silent drops early.

Error symptoms

  • Object keys with undefined values are completely absent from JSON.stringify output
  • API response body is missing fields that should be present but happen to be undefined
  • JSON.stringify(undefined) returns the JavaScript value undefined, not the string 'null' or 'undefined'
  • JSON.stringify([1, undefined, 3]) returns '[1,null,3]' — array slot becomes null silently
  • fetch() body is undefined when the serialized result is used directly without checking
  • Downstream consumers throw 'Cannot read property of undefined' because an expected key is absent

Common causes

  • A function returns undefined when it finds no result, and that value is assigned to an object property before serialization
  • Optional chaining (?.) returns undefined for a missing nested path, and the result goes directly into the response object
  • Express res.json() is called with an object that contains undefined fields from database queries or destructuring
  • TypeScript optional fields (field?: string) are never assigned, leaving them as undefined at runtime
  • Object spread merges two objects where one is missing a key, leaving the merged result with undefined at that key
  • An async function is awaited without checking for an empty result, and the undefined value propagates into the serialized object

When it happens

  • When building API response objects from database query results that may return fewer columns than expected
  • When serializing form data where optional fields were not filled in by the user
  • When an Express handler returns a partial update response and some fields are conditionally included
  • When logging request or response bodies that contain JavaScript objects with undefined values
  • When testing API responses and expecting fields that were silently dropped during serialization

Examples and fixes

An Express handler fetches a user record. If the database returns no phone number, the field is undefined and silently disappears from the JSON response, causing the client to throw when it tries to read it.

Silent drop of an API response field

❌ Wrong

app.get('/api/users/:id', async (req, res) => {
  const userRecord = await db.users.findById(req.params.id);
  // userRecord.phone may be undefined if not set
  const responseBody = {
    id: userRecord.id,
    email: userRecord.email,
    phone: userRecord.phone,   // potentially undefined
    createdAt: userRecord.createdAt
  };
  // phone key is silently omitted from the JSON output
  res.json(responseBody);
});

✅ Fixed

app.get('/api/users/:id', async (req, res) => {
  const userRecord = await db.users.findById(req.params.id);
  const responseBody = {
    id: userRecord.id,
    email: userRecord.email,
    phone: userRecord.phone ?? null,   // explicit null instead of undefined
    createdAt: userRecord.createdAt
  };
  // phone is always present in JSON: either a string or null
  res.json(responseBody);
});

The nullish coalescing operator (??) returns null when the left side is undefined or null, which JSON.stringify encodes correctly as the null token. The key is present in every response, and clients can check for null rather than checking for key existence. This makes the API contract explicit: phone is always returned, and its absence of a value is communicated as null rather than by omitting the key. The fix requires one character change per field but eliminates an entire class of silent data-loss bugs.

A round-trip through JSON.parse(JSON.stringify(obj)) is a simple technique to see exactly what survives serialization and what gets dropped.

Detecting undefined drops before they reach the client

❌ Wrong

const orderData = {
  orderId: 'ORD-2024-001',
  customerId: 'CUST-789',
  shippingAddress: userProfile.address,   // may be undefined
  discount: promotionCode?.amount,         // may be undefined
  notes: userInput.notes || undefined      // explicitly set to undefined
};

// Developer expects all keys to appear in logs
console.log(JSON.stringify(orderData));
// Output: {"orderId":"ORD-2024-001","customerId":"CUST-789"}
// shippingAddress, discount, notes are all gone

✅ Fixed

const orderData = {
  orderId: 'ORD-2024-001',
  customerId: 'CUST-789',
  shippingAddress: userProfile.address ?? null,
  discount: promotionCode?.amount ?? null,
  notes: userInput.notes || null
};

// Verify the shape survives serialization
const serialized = JSON.stringify(orderData);
const survived = JSON.parse(serialized);
console.log(Object.keys(survived));
// ['orderId', 'customerId', 'shippingAddress', 'discount', 'notes']
res.json(orderData);

The pattern of console.log(JSON.parse(JSON.stringify(obj))) is a quick way to see exactly which keys survive serialization during development. Any key absent from the parsed result was undefined before stringify. In the fixed version, every field is guaranteed to be either a real value or null, so all keys appear in every response. The notes field uses the || operator with null rather than undefined as the fallback, because the JSON null token is a valid field value that clients can reliably check.

Why undefined disappears from serialized objects

The JSON specification, defined in RFC 8259, enumerates exactly six value types: string, number, object, array, true, false, and null. The word undefined does not appear anywhere in the spec. This is not an oversight — JSON was designed as a language-independent format, and undefined is a concept specific to JavaScript and a handful of other languages. Languages like Python, Go, Java, and Rust have no equivalent concept.

When JSON.stringify encounters an object property whose value is undefined, it takes the only action that preserves spec compliance: it omits the key entirely. Including the key with a value of undefined is not possible because undefined is not a JSON value. Including it with null would silently change the meaning. So the key vanishes. No error, no warning, no indication that anything was lost.

Array slots behave differently. Because arrays preserve positional structure, omitting a slot would renumber everything after it and break length assumptions. Instead, JSON.stringify replaces undefined array elements with null. So JSON.stringify([1, undefined, 3]) produces '[1,null,3]' — the slot is filled with the closest JSON equivalent of nothing.

Calling JSON.stringify(undefined) directly, without wrapping it in an object or array, returns the JavaScript value undefined — not the string 'undefined', not 'null', not anything. If you assign this result to a variable and use it as a fetch body, you end up with undefined as the body, which is effectively no body at all. This is particularly insidious in test code where developers check the type of the serialization result and expect a string.

Understanding this behavior means accepting that JSON.stringify is not a faithful representation of every JavaScript value — it is a format converter that silently normalizes or drops values that have no JSON equivalent. Code that relies on the output of JSON.stringify must verify its shape before use, especially for objects built from optional data sources.

Finding silent undefined drops in API responses

Silent key drops are difficult to diagnose because the serialization succeeds — no error is thrown. The first sign is usually a client that receives a response missing an expected field and throws a TypeError like 'Cannot read property of undefined'. The client sees a field-absent response, but the server believes it sent the field because the server-side object had the key — it just had undefined as the value.

The fastest diagnostic is to console.log the object before serialization and then console.log the parsed serialized form side by side. Place this check in the request handler: const check = JSON.parse(JSON.stringify(responseBody)) and then compare Object.keys(responseBody) with Object.keys(check). Any key in the first set but not the second had an undefined value.

For TypeScript projects, a stricter type approach is to annotate API response objects with a type that includes null but excludes undefined. If you define type ApiUser = { phone: string | null } instead of { phone?: string }, the TypeScript compiler will flag any assignment of undefined to phone before the code reaches runtime. This moves the detection from the network layer back to the IDE.

In Express applications, you can add a middleware that intercepts outgoing JSON responses. By overriding res.json to compare the input object's key count with the serialized output's key count, you can emit a warning or throw when keys are dropped. This middleware approach catches the issue across all routes without modifying each handler individually. Combining it with structured logging allows you to record which routes are dropping which keys under which conditions.

Converting undefined to null before stringify

The most direct fix is to normalize undefined values to null at the point where the response object is assembled. The nullish coalescing operator (??) is the cleanest tool for this: someValue ?? null returns someValue if it is neither undefined nor null, and returns null otherwise. Apply it to every field that might be undefined before the object reaches JSON.stringify.

For objects with many optional fields, a utility function is cleaner than individual ?? applications on each key. A function like const nullifyUndefined = (obj) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, v === undefined ? null : v])) applies the transformation to every top-level key in one pass. For deeply nested objects, extend it to recurse into nested structures or use a library like lodash's mapValues.

A replacer function passed to JSON.stringify can also handle this inline: (key, value) => value === undefined ? null : value. This converts every undefined value to null during serialization without modifying the original object. The advantage is that the original object in memory retains its undefined values, which may be semantically important for code that runs after serialization. The disadvantage is that you must remember to apply the replacer at every JSON.stringify call site.

For fetch calls, a common bug is using JSON.stringify without checking whether the result is undefined. A guard like const body = JSON.stringify(payload); if (body === undefined) throw new Error('Payload is not serializable'); catches the case where the entire payload is undefined, which JSON.stringify converts to the JavaScript undefined value rather than a string. Always verify the return value of JSON.stringify before using it as a request body.

Optional TypeScript fields versus null-typed fields

TypeScript's type system distinguishes between optional fields and nullable fields, but both can lead to the same undefined-in-JSON problem at runtime. An optional field declared as phone?: string means the key may be absent from the object, and accessing it returns either a string or undefined. A nullable field declared as phone: string | null means the key is always present but may hold null as a value. JSON.stringify treats both situations differently: an absent key is never serialized, while a null value is serialized as the JSON null token.

When building API response types, the distinction matters enormously. If your OpenAPI spec says phone is nullable — meaning it is always present with either a string value or null — then the TypeScript type must be phone: string | null, not phone?: string. The optional syntax allows TypeScript code to assign undefined, which then gets silently dropped during serialization, violating the API contract.

The tsconfig option exactOptionalPropertyTypes, introduced in TypeScript 4.4, makes this stricter. When enabled, an optional field declared as phone?: string can receive a string but not explicitly undefined. Code like record.phone = undefined becomes a type error, which prevents the most common accidental undefined assignment. It does not prevent undefined from arriving via optional chaining or function return values, so it must be combined with runtime nullish coalescing rather than used as a complete solution.

The practical rule is: if a field appears in every response regardless of content, type it as field: ValueType | null and always assign either a real value or null. If a field is genuinely absent for certain objects — for example, a response type that has two variants — model it as a discriminated union or use separate response types rather than making every field optional. This approach makes the serialization contract visible in the type definitions and eliminates the ambiguity between missing and empty.

When Express res.json silently omits response fields

Express is the framework where this bug most commonly manifests because res.json is so convenient. A developer writes a route handler, assembles a response object from a database query, calls res.json, and ships the code. In testing with a complete database, every field is present and the response looks correct. In production with real data where some records have optional fields unset, the JSON response starts missing keys and clients begin throwing errors.

The issue is that Express calls JSON.stringify internally with no replacer, so any undefined field in the object passed to res.json is silently dropped. Express provides no warning, the HTTP response code is 200, and the content-length header reflects the actual shortened body. The only indication that something went wrong is the absence of a field that the API documentation says should always be present.

A common contributing factor is ORM result objects. Libraries like Prisma, Sequelize, and TypeORM return result objects where missing optional relations or columns may be undefined rather than null, depending on the query and the schema. When these result objects are passed directly to res.json without mapping to a response type, the undefined fields vanish. Always map ORM results to explicit response objects with controlled null handling rather than returning the ORM model instance directly.

Destructuring also introduces undefined silently. If you write const { name, phone, address } = userRecord and userRecord does not have a phone property, the destructured phone variable is undefined. Assigning it to the response object results in the same silent drop. Destructuring with defaults — const { phone = null } = userRecord — prevents this by substituting null for missing properties during destructuring, before the value reaches the response object.

Explicit null policies in API contracts

The most effective long-term fix is establishing an explicit null policy in your API contract and enforcing it at the type and validation layers. The policy has two rules: first, a field that should always be present in a response must always be present, with null representing the absence of a value. Second, a field that is sometimes present and sometimes absent should be modeled as a discriminated union or as a separate response variant, not as an optional undefined field in a shared type.

Document this policy in your API reference. If phone can be absent, your docs should say phone: string or null — not phone: string, optional. This communicates to consumers that checking for null is the correct pattern, not checking for key existence. It also communicates to the backend team that every serialization path must ensure the field is explicitly assigned null when there is no value.

Validate outgoing responses at runtime using a schema validator. Tools like Zod, Ajv, or Joi can validate that the response object matches the declared schema before serialization. If a required field is undefined, the validator throws before JSON.stringify has a chance to silently drop it. Place this validation in a response serializer layer that all route handlers use, rather than in individual handlers.

For teams migrating an existing API, an incremental approach works well: add a response validation middleware that logs warnings for undefined fields without rejecting the response. This surfaces which endpoints and which fields are affected without breaking clients immediately. Once all warnings are addressed by converting undefined to null or restructuring the response type, upgrade the middleware to throw on undefined values. This two-phase approach lets you clean up a large codebase safely without a flag day.

Quick fix checklist

  • Run JSON.parse(JSON.stringify(obj)) before sending and compare key counts to detect dropped fields
  • Replace undefined field values with null using the ?? operator at assignment time
  • Use a JSON.stringify replacer: (key, value) => value === undefined ? null : value for inline conversion
  • Type API response objects as field: Type | null rather than field?: Type to prevent optional undefined
  • Enable TypeScript's exactOptionalPropertyTypes to catch explicit undefined assignments at compile time
  • Destructure object properties with null defaults: const { phone = null } = record
  • Map ORM result objects to explicit response types before calling res.json()
  • Add response schema validation middleware that throws when required fields are undefined

Related guides

Frequently asked questions

Does JSON.stringify throw an error when it encounters undefined?

No. JSON.stringify never throws for undefined. It silently omits object keys whose values are undefined and replaces undefined array elements with null. Only calling JSON.stringify(undefined) directly produces a return value of undefined rather than a string — no error, just a non-string result that can cause issues if used as a fetch body or compared to a string.

Why does JSON.stringify handle undefined in arrays differently from objects?

Arrays preserve positional structure, so omitting an undefined element would shift all subsequent elements and change array length in ways that break consumer code. JSON.stringify replaces undefined array values with null to keep the slot filled. Object keys have no positional requirement, so dropping an undefined key is the least bad option — though it still causes silent data loss.

How can I tell which fields were dropped by JSON.stringify?

Compare Object.keys(originalObject) with Object.keys(JSON.parse(JSON.stringify(originalObject))). Any key present in the original but absent from the parsed result had an undefined value. This technique works for top-level keys. For nested objects, write a recursive comparison or use a schema validator that flags undefined values before serialization.

Should I always use null instead of undefined in JavaScript objects?

In data objects that will be serialized to JSON, yes — use null for absent values and avoid undefined. In internal application logic where JSON serialization is not involved, undefined is a legitimate signal for uninitialized or missing values. The practical rule is: convert to null at the boundary where data transitions from application state to JSON payload.

Why does my API return a field in dev but not in production?

Development databases often have more complete test data where optional fields are populated, so they produce non-undefined values. Production data has real records where optional fields may never have been set, returning undefined from queries. The JSON.stringify silent drop makes this look like a bug that appears only in production, when it is actually a serialization contract issue that was always present but masked by data completeness.

Does TypeScript prevent undefined from reaching JSON.stringify?

TypeScript can catch some cases at compile time if you type your response objects strictly with null rather than undefined for optional fields. The exactOptionalPropertyTypes compiler option makes this stricter. However, TypeScript cannot catch undefined that arrives at runtime through API calls, database queries, or dynamic property access — runtime validation with Zod or similar is required to catch those cases.

What happens when JSON.stringify(undefined) is used as a fetch body?

JSON.stringify(undefined) returns the JavaScript value undefined — not a string. Passing undefined as the fetch body either sends no body at all or causes a TypeError depending on the runtime. The server receives a request with no body or an empty body, which is almost certainly not what the client intended. Always guard with if (body === undefined) throw new Error before using a stringify result as a fetch body.

How does the nullish coalescing operator help with this issue?

The nullish coalescing operator (??) returns its right operand when the left is null or undefined, and returns the left operand otherwise. Writing field: someValue ?? null ensures that undefined becomes null before the object is assembled, so JSON.stringify sees null and encodes it as the JSON null token rather than silently dropping the key. It is more precise than the || operator, which also triggers for falsy values like 0, false, and empty string.

Can a JSON.stringify replacer convert undefined to null?

Yes. Pass (key, value) => value === undefined ? null : value as the replacer, and every undefined value in the object — at any nesting level — will be serialized as null rather than being dropped. The original object is not modified; the replacer only affects the string output. This is useful as a temporary measure while auditing an existing codebase, but replacing undefined with null at the source is cleaner long-term.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.