~/guides/json-stringify-null-undefined

JSON

JSON.stringify, null и undefined: три способа полю исчезнуть из результата

Разбор того, куда пропадают undefined значения при сериализации JSON, и почему это архитектурное решение, а не баг.

Объект в JavaScript имеет пять полей, а после JSON.stringify в результате их только четыре. Ни одной ошибки, ни одного предупреждения в консоли, поле просто исчезло, будто его никогда и не было. Это не баг движка и не баг конкретной библиотеки, это осознанное архитектурное решение спецификации JSON, которое неочевидно тем, кто не сталкивался с ним раньше.

Причина первая: undefined не существует в JSON вообще

Формат JSON, в отличие от JavaScript, не имеет понятия undefined как отдельного значения. У JSON есть null, строки, числа, булевы значения, массивы и объекты, и это исчерпывающий список. Undefined в эту схему просто не вписывается никак.

const data = { name: "web", port: undefined };
console.log(JSON.stringify(data));
// {"name":"web"}, поле port исчезло полностью, а не стало null

Обратите внимание, поле не превратилось в null, оно пропало из результата целиком, будто его не существовало на этапе сериализации.

Причина вторая: то же самое происходит внутри массивов, но иначе

const arr = [1, undefined, 3];
console.log(JSON.stringify(arr));
// [1,null,3], а вот здесь undefined стал null, а не пропал

Разница принципиальная: в объекте поле с undefined значением полностью удаляется из результата, а в массиве позиция с undefined заменяется на null, потому что удалить элемент массива без нарушения индексации остальных элементов невозможно, а объекту удаление ключа ничем не мешает структурно.

Контекст Значение undefined в результате
Поле объекта Полностью удаляется из вывода
Элемент массива Заменяется на null
Возвращаемое значение toJSON функции Зависит от того, что вернет сама функция

Причина третья: явный null сохраняется, в отличие от undefined

const data = { name: "web", port: null };
console.log(JSON.stringify(data));
// {"name":"web","port":null}, null сохраняется как есть

Здесь важно различать намерение: если поле явно равно null, это осознанное значение “известно, что значения нет”, и оно попадает в JSON как есть. А undefined это скорее “значение не задано вообще”, и именно поэтому оно не имеет прямого эквивалента в формате, у которого просто нет понятия “отсутствующее значение переменной”, есть только null как явное значение.

Как управлять этим поведением через replacer

Второй аргумент JSON.stringify позволяет явно контролировать, какие поля попадают в результат, вместо того чтобы полагаться на неявное поведение с undefined.

const data = { name: "web", secret: "12345", port: 8080 };

const json = JSON.stringify(data, (key, value) => {
  if (key === "secret") return undefined;
  return value;
});

console.log(json);
// {"name":"web","port":8080}, поле secret явно исключено через replacer

Такой подход куда надежнее, чем полагаться на то, что поле было undefined изначально, потому что явно показывает намерение исключить конкретное поле, а не оставляет это на волю случая в исходных данных.

Обратная сторона: JSON.parse не восстановит undefined

const restored = JSON.parse('{"name":"web"}');
console.log(restored.port);
// undefined, но по другой причине, поле просто отсутствует в объекте
console.log("port" in restored);
// false, ключа нет вообще, а не просто значение undefined

После цикла stringify и обратно parse невозможно отличить ситуацию “поле изначально было undefined” от ситуации “поля никогда не было в объекте”, обе схлопываются в одинаковый результат, отсутствие ключа.

Как быстро проверить свое предположение

Если сомневаетесь, какое именно поле пропадает и почему, вставьте объект в консоль браузера через JSON.stringify напрямую, либо для более сложных структур воспользуйтесь JSON Formatter, вставив уже готовый JSON и сверив число полей визуально с ожидаемым.

Итоговый чеклист

Undefined в поле объекта полностью удаляет это поле из результата JSON.stringify, а не превращает в null.

Undefined в элементе массива превращается именно в null, а не удаляется, потому что позиция элемента в массиве должна сохраниться.

Явный null всегда сохраняется в результате как есть, это осознанное значение, а не отсутствие значения.

Для явного контроля над тем, какие поля должны исключаться из сериализации, используйте replacer функцию вторым аргументом, а не полагайтесь на побочный эффект случайного undefined в исходных данных.

После цикла stringify и parse невозможно отличить, было ли поле undefined изначально или отсутствовало вообще, это важно помнить при отладке round-trip сериализации данных.