null Is Valid JSON, undefined Is Not — And the Difference Matters

Quick answer

💡JSON supports null as a first-class value per RFC 8259. JavaScript's undefined has no JSON representation. JSON.stringify silently removes object keys whose value is undefined, and converts undefined in arrays to null. Replace undefined with null or omit the field intentionally before serializing to avoid missing data in API responses.

Error symptoms

  • Object keys disappear from the serialized JSON string without any error thrown
  • API consumers receive an empty object {} when the original object had defined keys with undefined values
  • Array positions shift unexpectedly because undefined array elements become null in JSON output
  • JSON.parse('undefined') throws SyntaxError: Unexpected token u in JSON at position 0
  • TypeScript optional properties typed as string | undefined are serialized differently than string | null properties
  • Backend data from a database returns null but frontend code checks for undefined and misses the null case

Common causes

  • Accessing a property that does not exist on an object returns undefined in JavaScript, and passing that value to JSON.stringify drops it silently
  • Destructuring with default values can produce undefined when the source object explicitly sets a field to undefined
  • TypeScript optional properties use undefined as their absent sentinel, which does not match JSON's absence convention
  • Database queries return null for missing fields, but JavaScript code sets fields to undefined during processing
  • A function that forgets to return a value implicitly returns undefined, which disappears when the result is embedded in a JSON object
  • Deleting a property with the delete operator leaves the key absent, but setting it to undefined keeps the key with an unserializable value

When it happens

  • When building a user profile API where some fields are optional and the backend uses undefined to represent absence
  • When transforming database rows into API response objects and some nullable columns become undefined during the mapping
  • When frontend state management stores form fields as undefined for untouched inputs before sending the form data as JSON
  • When chaining optional property accesses with ?. and the result is undefined because an intermediate object was missing
  • When a middleware layer processes request bodies and sets fields to undefined as a way to signal that a value should be ignored

Examples and fixes

When an object contains properties set to undefined, JSON.stringify removes them entirely from the output. This is silent — no error is thrown, and the caller has no indication that data was lost.

undefined keys silently vanish from JSON.stringify output

❌ Wrong

const userProfile = {
  id: 'usr_4821',
  name: 'Sarah Chen',
  email: '[email protected]',
  phoneNumber: undefined,
  lastLoginAt: undefined,
  preferences: {
    theme: 'dark',
    newsletter: undefined
  }
};

const serialized = JSON.stringify(userProfile);
console.log(serialized);
// Output: {"id":"usr_4821","name":"Sarah Chen","email":"[email protected]","preferences":{"theme":"dark"}}
// phoneNumber, lastLoginAt, and newsletter are completely missing

✅ Fixed

const rawProfile = {
  id: 'usr_4821',
  name: 'Sarah Chen',
  email: '[email protected]',
  phoneNumber: undefined,
  lastLoginAt: undefined,
  preferences: {
    theme: 'dark',
    newsletter: undefined
  }
};

// Replace undefined with null before serializing
function replaceUndefinedWithNull(obj) {
  return JSON.parse(JSON.stringify(obj, (key, value) =>
    value === undefined ? null : value
  ));
}

const userProfile = replaceUndefinedWithNull(rawProfile);
console.log(JSON.stringify(userProfile, null, 2));
// All keys preserved, undefined fields now show as null

The broken version relies on the implicit behavior of JSON.stringify, which silently drops keys whose values are undefined. The API consumer receives fewer fields than expected and cannot distinguish between a field that was intentionally absent and one that was present but undefined. The fixed version uses a replacer function inside JSON.stringify to intercept undefined values and convert them to null before serialization, then parses the result back into an object. This ensures all fields appear in the output, and consumers can check for null rather than missing keys. The replacer function approach works recursively through nested objects automatically.

After parsing JSON, all absent-value fields will be null, never undefined. Code that checks for undefined after JSON.parse will always get the wrong answer for nullable JSON fields.

Handling null from JSON.parse correctly versus undefined checks

❌ Wrong

const apiResponse = JSON.parse(
  '{"userId":"acct_9934","displayName":"Marcus Webb","deletedAt":null}'
);

// Wrong check: null !== undefined, this will always be false
if (apiResponse.deletedAt === undefined) {
  console.log('Account is active');
} else {
  console.log('Account state unknown');
}

// Also wrong: accessing non-existent key returns undefined, not null
const missingField = apiResponse.nonExistentField;
console.log(missingField === null); // false — it's undefined

✅ Fixed

const apiResponse = JSON.parse(
  '{"userId":"acct_9934","displayName":"Marcus Webb","deletedAt":null}'
);

// Correct: JSON fields that exist will never be undefined after parse
if (apiResponse.deletedAt === null) {
  console.log('Account is active (deletedAt explicitly null)');
} else if (apiResponse.deletedAt !== undefined) {
  console.log('Account deleted at:', apiResponse.deletedAt);
}

// Use hasOwnProperty or 'in' operator to check field presence
const hasDeletedAt = 'deletedAt' in apiResponse;
console.log('Field present in response:', hasDeletedAt); // true

After calling JSON.parse, fields that were present in the JSON string will exist as own properties on the result object. Fields that were set to null in the JSON will be JavaScript null, never undefined. Accessing a field that was not present in the original JSON returns JavaScript undefined because the property does not exist on the object at all. The correct pattern is to check for null when a field was present, use the in operator or hasOwnProperty to test whether a field was included, and reserve undefined checks for the case where you are checking whether an object has a property at all. Mixing these checks produces subtle bugs that only manifest with certain data shapes.

How JSON handles null versus undefined in the spec

RFC 8259, the specification that defines JSON, enumerates exactly six types of values: strings, numbers, the boolean literals true and false, arrays, objects, and the literal null. The word undefined appears nowhere in the RFC because undefined is a JavaScript language concept, not a data interchange concept. When the JSON specification was designed, it drew inspiration from JavaScript's object literal syntax but intentionally excluded language-specific runtime values that have no portable representation.

Null in JSON is a first-class value written as the four-character literal null without any quotes. It means precisely what the specification says: a JSON value that intentionally represents the absence or emptiness of a meaningful value. A field set to null signals to the consumer that the field exists as a concept in the data model but currently has no value. This is semantically different from a field that is not present at all, though both are valid JSON and parsers accept both.

Undefined, by contrast, is a JavaScript runtime value that has no equivalent representation in any other language or data format. It exists in JavaScript to represent the state of a variable that has been declared but not assigned, a function parameter that was not passed, or a property accessed on an object that does not have that property. Because undefined carries meaning only within a JavaScript runtime context, it cannot be meaningfully transmitted to another system, which is why the JSON specification excludes it.

The JSON.stringify function enforces this boundary by applying different rules depending on where an undefined value appears. When undefined appears as the value of an object property, JSON.stringify removes the entire key-value pair from the output without warning. When undefined appears as an element in an array, JSON.stringify converts it to null, preserving the array's length and the positional meaning of remaining elements. When JSON.stringify is called with undefined as the top-level value, it returns the JavaScript value undefined rather than a string, which is a subtle signal that the result should not be used as a JSON string.

Understanding these rules is essential for building reliable APIs. A backend that sets fields to undefined rather than null before serializing will produce JSON that omits those fields entirely, which clients will interpret as the field not being part of the data model at all. If the client then makes a decision based on whether the field is present, it will behave differently than if the field had been explicitly set to null, even though the developer's intent was the same in both cases.

Spotting silent undefined drops during JSON serialization

The most direct way to discover undefined-related data loss is to compare the original JavaScript object with the JSON.stringify output. If the counts of keys differ between the original object and the parsed result of stringifying and reparsing it, some keys were dropped. A simple diagnostic function can traverse the object and collect all paths where the value is undefined, giving a clear list of which fields will be lost.

Another useful diagnostic is to call JSON.stringify with a replacer function that logs every key-value pair it processes. The replacer receives each key and value before the value is serialized. If you log every call where the value is undefined, you will see exactly which fields are being silently dropped. This approach works well during debugging sessions but should be removed in production code for performance reasons.

TypeScript can help catch undefined-related serialization issues at compile time if types are defined carefully. Declaring a field as string | null tells TypeScript that null is a valid value for that field and will appear in the JSON output. Declaring it as string | undefined tells TypeScript that the field may be absent, which is correct from a JavaScript perspective but misleading for JSON serialization because it implies the field will be omitted from the output rather than set to null. Adding a custom ESLint rule or a type-level transform that maps TypeScript's optional properties to null before serialization can prevent these issues from reaching production.

For debugging API responses, comparing the request payload that your code generates against what the server actually receives is often the most efficient approach. Many HTTP debugging tools like curl with verbose output, the Network tab in browser DevTools, or a local HTTP proxy like mitmproxy will show the raw JSON string that was sent. If you see fewer fields in the wire format than you expected to send, undefined-dropping is a likely cause. Checking the size of the JSON string against an estimate based on the number of fields is another quick heuristic: if the string is significantly shorter than expected, fields are missing.

Replacing undefined values before serializing to JSON

The most controlled fix is to normalize the object before calling JSON.stringify, converting every undefined value to null. This makes the intent explicit: fields that have no value will appear in the JSON output as null rather than disappearing. The transformation can be applied with a recursive function that walks the object tree, or more concisely using JSON.stringify's replacer function parameter.

Using the replacer function approach, you pass a function as the second argument to JSON.stringify. The replacer receives the key and value for every entry it visits, including nested objects. If the value is undefined, return null instead. For all other values, return the value unchanged. This approach handles arbitrary nesting without requiring a separate recursive traversal function.

If you want to omit fields entirely rather than include them as null, the replacer can return undefined explicitly for those fields, which signals to JSON.stringify to skip them. This is the default behavior for undefined values, but making it explicit in a replacer function at least documents the intent rather than relying on implicit behavior. The distinction matters for API design: omitting a field entirely versus setting it to null communicates different semantics to the consumer.

For TypeScript projects, a common pattern is to define two separate types: a domain type that uses undefined for optional fields because that matches JavaScript's native representation of optional object properties, and a serialization type that uses null instead of undefined and omits certain internal fields. A transformation function converts from the domain type to the serialization type before calling JSON.stringify. This approach keeps the JavaScript code idiomatic while ensuring the JSON output is consistent and explicit.

In frameworks that use class-based serialization, such as NestJS with class-transformer, you can use the Transform decorator to convert undefined values to null at the class property level. The serialization library handles the conversion automatically when transforming class instances to plain objects before JSON serialization. This approach centralizes the undefined-to-null conversion in the data model definition rather than scattering it throughout the codebase.

TypeScript types across the null and undefined boundary

TypeScript treats null and undefined as distinct types, and the distinction affects how optional properties and nullable properties are modeled in type definitions. When strict null checks are enabled in tsconfig.json, which is the default in modern TypeScript projects, null and undefined are separate types that must be explicitly included in a union if they are allowed as values for a property.

A property typed as name: string | null expresses that name will always be present in the object but may be null. This maps cleanly to JSON because null is a valid JSON value. When serialized, the field will appear in the JSON output either as a JSON string or as the literal null. Consumers of the JSON can rely on the field always being present and always being either a string or null.

A property typed as name?: string, which is shorthand for name: string | undefined, expresses that the property may not exist on the object at all. In JavaScript this is useful, but in JSON serialization it creates ambiguity: will the JSON output contain the field set to null, will it omit the field entirely, or will the serialization behavior depend on whether the JavaScript object had the property at all? The answer depends on how the object was created and whether any normalization step converted undefined to null before serialization.

A property typed as name?: string | null combines both optional (may not exist) and nullable (may be null if it exists). This is the most permissive type and the most ambiguous for serialization, because it allows three distinct states: the property is absent, the property exists and is null, or the property exists and has a string value. JSON serialization must decide which of these three states to represent as which JSON output, and without explicit normalization the implicit JSON.stringify behavior may not match the developer's intent.

When designing TypeScript types for JSON API contracts, prefer string | null over string | undefined for fields that should always appear in the serialized output. Reserve undefined for fields that should be omitted from the JSON entirely, and handle that omission explicitly in a serialization layer rather than relying on JSON.stringify's implicit dropping behavior.

When APIs return null versus omit the field entirely

API design conventions around null versus field omission are not universally agreed upon, but there are two coherent schools of thought that both work well when applied consistently. The first convention is to always include every documented field in the response, setting it to null when the field has no value. This makes the API schema stable and predictable: consumers always receive the same set of keys in every response and can validate the response structure without accounting for optional fields that may or may not appear. GraphQL APIs generally follow this convention.

The second convention is to omit fields when they have no value, treating a missing field and null as equivalent signals for absence. This produces smaller JSON responses because empty fields consume no space, and it avoids the philosophical question of whether null and missing-field mean the same thing. REST APIs using this convention often document that absent fields should be treated as null by consumers. The risk is that consumers cannot distinguish between an intentionally null field and a field that does not exist in the current API version.

The most problematic API design is inconsistency: sometimes returning null for absent fields and sometimes omitting them, with no documented rule for which behavior applies where. Consumers must then write defensive code that checks both for null and for property absence on every optional field, doubling the conditional logic for handling nullable data. Code that was written to handle one convention will produce subtle bugs when encountering responses that follow the other convention.

For frontend code consuming an API, using optional chaining with a null coalescing fallback handles both conventions gracefully: apiResponse.user?.deletedAt ?? null will return null whether deletedAt was explicitly null or was absent from the response object. This defensive coding style lets the frontend tolerate both JSON conventions without requiring the API design to be perfectly consistent, which is practical for real-world projects where APIs evolve over time and consistency is not always maintained.

Designing nullable fields clearly in JSON schemas

JSON Schema provides a formal way to document whether a field can be null and whether a field is required to be present in a JSON document. These two concerns are separate in JSON Schema, and confusing them leads to schemas that allow invalid data or reject valid data. Defining them explicitly prevents the null-versus-undefined confusion from spreading from JavaScript code into the API contract.

In JSON Schema draft-07, a field that can be either a string or null is typed as { "type": ["string", "null"] }. An alternative notation using oneOf with one branch being { "type": "null" } achieves the same validation but is more verbose. The required array on an object schema determines which fields must be present. A field can be required and nullable, meaning it must appear in the JSON but its value can be null. A field can be optional and nullable, meaning it may be absent or present as null. A field can be required and non-nullable, meaning it must appear and must be a non-null string.

Draft 2019-09 and 2020-12 introduced the explicit nullable approach of using a oneOf with null, which is more expressive and aligns better with how validators process schemas. OpenAPI 3.0 uses a separate nullable: true attribute alongside the type definition, which is specific to OpenAPI's subset of JSON Schema. OpenAPI 3.1 adopts the full JSON Schema 2020-12 approach, using type arrays to express nullability, making the schemas more standard and interoperable.

When documenting an API, making the null-versus-omission convention explicit in the schema prevents consumer confusion. If a field will always appear in the response but may be null, mark it as required in the JSON Schema with a nullable type. If a field may be absent entirely, mark it as not required and document whether absence is equivalent to null or has distinct semantics. If the API follows the omit-when-null convention, document this as a global convention so consumers do not need to check each field's individual documentation.

Code generation tools like openapi-generator and quicktype read JSON Schema and generate TypeScript types from it. If the schema uses nullable types correctly, the generated types will use string | null rather than string | undefined for nullable fields, which propagates the correct serialization behavior throughout the frontend codebase automatically without requiring manual type annotations.

Quick fix checklist

  • Search your serialization code for undefined values that should be null before calling JSON.stringify
  • Use a replacer function in JSON.stringify to convert undefined to null rather than silently dropping keys
  • Confirm that JSON.parse output is checked with null comparisons, not undefined comparisons
  • Use the in operator or hasOwnProperty to test field presence, not equality with undefined
  • In TypeScript, prefer string | null over string | undefined for fields that should appear in JSON output
  • Document whether your API omits null fields or includes them explicitly, and apply that rule consistently
  • Validate API responses against a JSON Schema that explicitly marks required and nullable fields
  • Add integration tests that verify response shapes contain the expected keys even when values are null

Related guides

Frequently asked questions

Why does JSON.stringify drop undefined keys instead of converting them to null?

The JSON specification does not include undefined as a value type. Rather than throwing an error when it encounters undefined, JSON.stringify silently drops object keys with undefined values to produce valid JSON. For arrays, it converts undefined to null to preserve positional indices. This behavior is intentional but surprising to developers who expect the output to mirror the input structure exactly.

What does JSON.stringify return when called with just undefined?

JSON.stringify(undefined) returns the JavaScript value undefined, not a string. This is one of the few cases where JSON.stringify returns something other than a string. If you assign the result to a variable and try to use it as a JSON string, you will get a TypeError or unexpected behavior. Always ensure the top-level value passed to JSON.stringify is a valid JSON-serializable value.

Does JSON.parse ever return undefined?

JSON.parse never returns undefined for a valid JSON input because undefined is not a valid JSON value. It returns null for the JSON literal null, strings for JSON strings, numbers for JSON numbers, booleans for JSON booleans, and objects or arrays for JSON objects and arrays. If you pass the string 'undefined' to JSON.parse, it throws a SyntaxError because that is not valid JSON syntax.

How do I preserve all keys including those set to undefined when serializing?

Pass a replacer function as the second argument to JSON.stringify that converts undefined to null: JSON.stringify(obj, (key, value) => value === undefined ? null : value). This ensures all keys appear in the output. If you want to omit keys selectively rather than preserve them all, return undefined from the replacer for specific keys you want excluded, which makes the omission explicit rather than implicit.

In TypeScript, what is the difference between string | null and string | undefined for JSON?

A TypeScript property typed as string | null will serialize to a JSON string or the JSON literal null when passed to JSON.stringify. A property typed as string | undefined will serialize to a JSON string when defined, but the entire key will be omitted from the JSON output when the value is undefined. For fields that should always appear in JSON responses, always use string | null rather than string | undefined.

Should my API return null or omit the field when there is no value?

Both conventions are valid, but choose one and apply it consistently. Returning null makes the schema stable and lets consumers validate response shapes reliably. Omitting the field produces smaller responses but requires consumers to handle missing keys defensively. Document your convention in the API specification so clients know whether a missing field and a null field are semantically equivalent or distinct.

How does optional chaining interact with null versus undefined?

The optional chaining operator ?. short-circuits and returns undefined when the left-hand side is null or undefined. This means apiResponse.user?.name returns undefined whether user is null or absent, losing the distinction between null and missing. If you need to preserve the null signal for JSON serialization, use explicit null checks rather than optional chaining, or use the null coalescing operator ?? to normalize undefined back to null after optional chaining.

Can I use JSON.stringify to detect which fields in an object are undefined?

Yes, indirectly. You can compare the keys of the original object against the keys of the parsed result of JSON.stringify to find which keys were dropped. Alternatively, iterate Object.keys(obj) and check which keys have undefined values directly. For a cleaner approach, use a replacer function that logs or collects every key where the value is undefined before the drop occurs, giving you explicit visibility into what will be lost during serialization.

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