881 words
4 minutes

JSON.stringify Drops undefined: Keep Intentional Null Fields

2026-10-09
Frontend
JavaScript
/
Node.js
/
API
/
Troubleshooting

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: undefined
missing 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 passed

Object.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 helperOutgoing JSONIntended 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 falseThrows TypeErrorReject 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:

JSON.stringify Drops undefined: Keep Intentional Null Fields
https://laplusda.com/en/posts/json-stringify-undefined-null-fields/
Author
Zero
Published at
2026-10-09
License
CC BY-NC-SA 4.0