JSON.stringify Drops undefined: Keep Intentional Null Fields
JSON.stringify({ nickname: undefined }) returns '{}'. The property exists in the JavaScript object, but it disappears from the JSON text. If your request is meant to clear a nickname, the server never receives that field.
Choose the outgoing meaning before serialization. Send null only when the endpoint accepts it for that field; omit the property when omission is the intended operation. JSON cannot preserve JavaScript’s undefined value as a distinct JSON value.
Identify what disappeared
The ECMAScript serialization rules distinguish three positions. An object property with an undefined value is omitted. An array element becomes null. A root undefined produces the JavaScript value undefined, rather than JSON text.
The fixture below verifies these boundaries and the proposed conversion. For a plain object whose undefined fields should all become null, use a strict comparison in the replacer:
const form = { nickname: undefined, active: false, retries: 0, note: '' };const body = JSON.stringify(form, (_key, value) => value === undefined ? null : value);console.log(body);// {"nickname":null,"active":false,"retries":0,"note":""}This deliberately changes the data contract: after parsing, nickname is null. It does not restore undefined. An absent key is still absent because the replacer cannot supply fields that serialization never visits.
Avoid value || null. In this example, it also changes false, 0, and '' to null. Those values may carry the reader’s intended update just as much as a nonempty string does.
Reproduce the request-body boundary
Save this as json-fields.mjs and run node json-fields.mjs. The exact fixture was executed during article preparation on macOS with Node.js 24.13.0 on October 9, 2026. It uses assertions and no external endpoint or package.
import assert from 'node:assert/strict';
const input = { nickname: undefined, active: false, retries: 0, note: '' };const omitted = JSON.stringify(input);const explicit = JSON.stringify(input, (_key, value) => value === undefined ? null : value);assert.equal(omitted, '{"active":false,"retries":0,"note":""}');assert.equal(explicit, '{"nickname":null,"active":false,"retries":0,"note":""}');assert.equal(Object.hasOwn(JSON.parse(omitted), 'nickname'), false);assert.equal(JSON.parse(explicit).nickname, null);console.log(`object default: ${omitted}`);console.log(`object replacer: ${explicit}`);
const array = JSON.stringify([undefined, null, false, 0, '']);assert.equal(array, '[null,null,false,0,""]');assert.equal(JSON.stringify(undefined), undefined);assert.equal(JSON.stringify(undefined, (_key, value) => value === undefined ? null : value), 'null');assert.equal(JSON.stringify({}, (_key, value) => value === undefined ? null : value), '{}');console.log(`array default: ${array}`);console.log(`root default type: ${typeof JSON.stringify(undefined)}`);console.log('missing key with replacer: {}');
const bad = JSON.stringify(input, (_key, value) => value || null);assert.equal(bad, '{"nickname":null,"active":null,"retries":null,"note":null}');console.log(`falsy replacer: ${bad}`);
function nicknamePatch(form) { if (!Object.hasOwn(form, 'nickname')) return {}; const value = form.nickname; if (value === undefined || value === null || value === '') { return { nickname: null }; } if (typeof value !== 'string') throw new TypeError('nickname must be a string'); return { nickname: value };}for (const [form, expected] of [ [{}, '{}'], [{ nickname: undefined }, '{"nickname":null}'], [{ nickname: null }, '{"nickname":null}'], [{ nickname: '' }, '{"nickname":null}'], [{ nickname: 'Ada' }, '{"nickname":"Ada"}'],]) { const body = JSON.stringify(nicknamePatch(form)); assert.equal(body, expected); console.log(`patch: ${body}`);}assert.throws(() => nicknamePatch({ nickname: false }), TypeError);assert.equal(Object.hasOwn(input, 'nickname'), true);console.log('all assertions passed');Expected output:
object default: {"active":false,"retries":0,"note":""}object replacer: {"nickname":null,"active":false,"retries":0,"note":""}array default: [null,null,false,0,""]root default type: undefinedmissing key with replacer: {}falsy replacer: {"nickname":null,"active":null,"retries":null,"note":null}patch: {}patch: {"nickname":null}patch: {"nickname":null}patch: {"nickname":null}patch: {"nickname":"Ada"}all assertions passedObject.hasOwn() checks whether the parsed object contains the field. Reading only parsed.nickname would return undefined both for an absent property and for an own property whose value is undefined. The assertion on the input also confirms that serialization did not delete its original property.
Make null a field decision
The nicknamePatch() function implements an illustrative endpoint contract: omission leaves the nickname untouched, null requests clearing it, and a string replaces it. These meanings are application choices, not behavior imposed by JSON.stringify() or by the HTTP method.
| Input to this helper | Outgoing JSON | Intended operation in this example |
|---|---|---|
No own nickname property | {} | Leave unchanged |
Own property set to undefined, null, or '' | {"nickname":null} | Request clearing |
Own property set to 'Ada' | {"nickname":"Ada"} | Replace |
Own property set to false | Throws TypeError | Reject invalid field type |
Each row is exercised in the fixture. The helper treats an empty string as a clearing request and accepts whitespace as a string; change those choices if the endpoint’s validation differs. The object must be supplied by the caller; this helper does not validate an entire request schema.
Use this field policy when only selected nullable fields should be converted. A global replacer would convert every encountered undefined value, including nested ones, and could send null to fields that reject it. Verify the endpoint’s accepted body and clearing semantics before applying either approach.
Inspect the serialized body before debugging the server
Compare the form object, the actual JSON text, and Object.hasOwn(JSON.parse(body), 'nickname'). If the text lacks the field, inspect the object builder and serializer first. If the text contains "nickname":null but the stored value remains unchanged, the local fixture cannot establish why: check request validation and the endpoint’s update contract.
MDN documents the replacer and toJSON() hooks. A custom toJSON() runs before the replacer, so it may already have removed information. A replacer is not a general repair for every missing property.
This article’s executed examples cover plain objects, arrays, a root undefined value, and the field helper. The published fixture does not exercise custom serialization hooks, browser transport, a deployed API, or database updates. For a response that arrives but fails HTTP or JSON checks, use the separate fetch status and parsing diagnosis.
Define the request meaning at the field boundary, then assert on the JSON text. Explicit null preserves a clearing signal only when your endpoint defines that signal.
References: