Объект в 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 сериализации данных.