Json.parse та json.stringify в JavaScript
JSON.stringify() перетворює JavaScript-значення на JSON-рядок. JSON.parse() перетворює JSON-рядок назад на JavaScript-значення.
Теорія
TL;DR
stringify"пакує" об'єкт у звичайний текстовий рядок для передачі;parse"розпаковує" його назад у робочий об'єкт.stringifyтихо відкидає функції,undefinedта Symbol; Dates стають ISO-рядками;BigIntодразу кидаєTypeError.parseперевіряє кожен символ за специфікацією RFC 8259 і кидаєSyntaxErrorна будь-яких відхиленнях.- Використовуй обидва для: тіла API-запитів, localStorage, конфіг-файлів.
- Уникай для: об'єктів Date, функцій, класів, циклічних посилань.
Швидкий приклад
const user = { name: 'Alice', age: 30, active: true };
const json = JSON.stringify(user);
// '{"name":"Alice","age":30,"active":true}'
const parsed = JSON.parse(json);
console.log(parsed.name); // 'Alice'
console.log(typeof json); // 'string'
console.log(typeof parsed); // 'object'stringify повертає звичайний рядок без методів. parse повертає об'єкт, з яким можна нормально працювати.
Ключова різниця
JSON.stringify() будує рядок, рекурсивно обходячи значення та викликаючи toJSON() на об'єктах де він є. Все без JSON-еквівалента - функції, Symbol, undefined - відкидається з об'єктів або замінюється null у масивах. JSON.parse() робить зворотне, але перевіряє вхід за RFC 8259. Одинарні лапки, ключі без лапок, зайві коми - все це кидає SyntaxError. Round-trip не є повним: втрачені дані не повернеш через parse.
Коли використовувати
- API-запити:
JSON.stringify(body)перед відправкою,response.json()після отримання. - localStorage:
setItem('key', JSON.stringify(data))для збереження,JSON.parse(getItem('key'))для читання. - Дебаг-логи:
JSON.stringify(obj, null, 2)для читабельного знімку стану. - Конфіг у Node.js:
JSON.parse(fs.readFileSync('config.json', 'utf8'))при старті. - Уникай для: Date, функцій, класів, циклічних посилань,
BigInt.
Що stringify змінює або відкидає
const obj = {
fn: () => 'hello', // відкидається з об'єкта
undef: undefined, // відкидається з об'єкта
sym: Symbol('id'), // відкидається з об'єкта
date: new Date('2023-01-01'), // стає ISO-рядком
nan: NaN, // стає null
};
JSON.stringify(obj);
// '{"date":"2023-01-01T00:00:00.000Z","nan":null}'
// У масивах undefined та функції стають null:
JSON.stringify([1, undefined, () => {}, 3]);
// '[1,null,null,3]'
// BigInt кидає одразу - жодної тихої конвертації:
JSON.stringify({ id: 123n }); // TypeError: Do not know how to serialize a BigIntreplacer та reviver
Обидва методи мають необов'язковий параметр для нестандартних сценаріїв. replacer у stringify фільтрує або трансформує значення на виході. reviver у parse відновлює типи на вході.
// replacer як масив: whitelist властивостей
JSON.stringify({ name: 'Bob', password: 'secret' }, ['name']);
// '{"name":"Bob"}'
// replacer як функція: обробка BigInt
JSON.stringify({ id: 123n }, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"id":"123"}'
// reviver: відновити Date після parse
const json = '{"name":"Alice","birthday":"1990-06-15T00:00:00.000Z"}';
const result = JSON.parse(json, (key, value) =>
key === 'birthday' ? new Date(value) : value
);
console.log(result.birthday instanceof Date); // trueReplacer викликається зверху вниз від кореня; reviver - знизу вгору, спочатку вкладені властивості.
Як це працює у V8
У V8 JSON.stringify() використовує C++ серіалізатор, який обходить граф об'єктів, записує примітиви напряму і викликає toJSON() де він є. JSON.parse() - це рекурсивний descent-парсер, що перевіряє UTF-8 та синтаксис за RFC 8259. Обидва методи працюють у нативному коді. Тому вони швидші за будь-яку бібліотеку для JSON написану на JavaScript.
Типові помилки
1. Забути зробити parse після читання з localStorage
localStorage.setItem('user', JSON.stringify({ name: 'Alice' }));
// Неправильно
const raw = localStorage.getItem('user');
console.log(raw.name); // undefined - raw це рядок, не об'єкт
// Правильно
const user = JSON.parse(localStorage.getItem('user'));
console.log(user.name); // 'Alice'Ця помилка - одна з найпоширеніших у продакшен-коді. localStorage завжди повертає рядки.
2. Очікувати, що Date переживе серіалізацію
const obj = { birthday: new Date('1990-01-01') };
const clone = JSON.parse(JSON.stringify(obj));
console.log(clone.birthday instanceof Date); // false
console.log(clone.birthday); // '1990-01-01T00:00:00.000Z'
// clone.birthday.getFullYear() → TypeError
// Виправлення: reviver
const restored = JSON.parse(JSON.stringify(obj), (k, v) =>
k === 'birthday' ? new Date(v) : v
);
console.log(restored.birthday instanceof Date); // true3. Циклічні посилання ламають stringify
const obj = { x: 1 };
obj.self = obj;
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
// Виправлення: replacer з WeakSet
function safeStringify(value) {
const seen = new WeakSet();
return JSON.stringify(value, (key, val) => {
if (typeof val === 'object' && val !== null) {
if (seen.has(val)) return '[Circular]';
seen.add(val);
}
return val;
});
}4. Передати не-рядок у JSON.parse()
JSON.parse({ a: 1 }); // SyntaxError
// Об'єкт зводиться до рядка "[object Object]", який не є валідним JSON.
// Передавай тільки рядок:
JSON.parse('{"a":1}'); // { a: 1 }5. Використовувати JSON для deep clone при наявності спеціальних типів
const original = { date: new Date(), map: new Map([[1, 'one']]) };
const broken = JSON.parse(JSON.stringify(original));
// broken.date - рядок, broken.map - порожній об'єкт {}
// Краще використати structuredClone():
const proper = structuredClone(original);
console.log(proper.date instanceof Date); // true
console.log(proper.map instanceof Map); // trueДе зустрічається
- React: збереження стану в localStorage через
stringifyпри зміні,parseпри монтуванні (типово для теми інтерфейсу в Next.js). - Express:
express.json()автоматично парсить тіло JSON-запитів;res.json()автоматично серіалізує відповідь. - Node.js:
fs.writeFileSync('config.json', JSON.stringify(config, null, 2))для читабельних конфіг-файлів. - Fetch API:
body: JSON.stringify(data)у POST-запитах,await response.json()для відповіді. - Redux Persist: серіалізує зрізи стору в localStorage між сесіями.
Питання на співбесіді
Q: Що відбувається з undefined в об'єкті та в масиві?
A: В об'єкті ключ відкидається повністю: JSON.stringify({a: undefined}) дає '{}'. В масиві undefined стає null, щоб зберегти позиції елементів: JSON.stringify([undefined]) дає '[null]'.
Q: replacer приймає і функцію, і масив. Коли вибирати масив?
A: Масив - для простого whitelist властивостей: JSON.stringify(user, ['name', 'email']). Функція - коли потрібна умовна логіка або трансформація типів на вкладених рівнях.
Q: Яка різниця між JSON.parse() та eval()?
A: eval() виконує довільний JavaScript, що небезпечно з ненадійними даними. JSON.parse() приймає тільки валідний JSON і не може запускати код. Для зовнішніх даних завжди використовуй JSON.parse().
Q: Як JSON.parse() обробляє дублікати ключів у JSON-рядку?
A: Перемагає останнє значення. JSON.parse('{"a":1,"a":2}') повертає {a: 2}. Специфікація дозволяє дублікати, але їх наявність завжди є помилкою в джерелі.
Q: (Senior) Реалізуй stringify для циклічних посилань без бібліотек.
A: Відстежуй відвідані об'єкти через WeakSet. У функції-replacer перевіряй чи значення вже є у множині перед додаванням. WeakSet підходить тут тому, що тримає посилання слабко і не блокує збирач сміття.
Приклади
localStorage: збереження та відновлення налаштувань
const settings = { theme: 'dark', fontSize: 16, notifications: true };
// Зберегти
localStorage.setItem('settings', JSON.stringify(settings));
// Відновити (перевір на null - ключа може не бути)
const raw = localStorage.getItem('settings');
const loaded = raw !== null ? JSON.parse(raw) : settings;
console.log(loaded.theme); // 'dark'Завжди перевіряй на null перед JSON.parse(). localStorage.getItem() повертає null для відсутніх ключів, а звернення до властивостей null пізніше кине помилку.
POST-запит через Fetch
async function createUser(data) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) // об'єкт → рядок для мережі
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // парсить тіло відповіді
}
createUser({ name: 'Alice', role: 'developer' })
.then(user => console.log('Створено з id:', user.id));response.json() - це фактично JSON.parse(await response.text()). Якщо тіло відповіді не є валідним JSON, метод кидає помилку. Тому спочатку перевіряй response.ok.
replacer та reviver для нестандартних сценаріїв
// stringify: прибрати чутливі поля, обробити BigInt
const payload = {
userId: 9007199254740993n, // завеликий для звичайного JS-числа
name: 'Bob',
password: 'secret'
};
const serialized = JSON.stringify(payload, (key, value) => {
if (key === 'password') return undefined; // прибрати
if (typeof value === 'bigint') return value.toString(); // конвертувати
return value;
});
// '{"userId":"9007199254740993","name":"Bob"}'
// parse: відновити Date з відповіді API
const apiResponse = '{"id":1,"createdAt":"2023-06-15T10:00:00.000Z"}';
const record = JSON.parse(apiResponse, (key, value) =>
key === 'createdAt' ? new Date(value) : value
);
console.log(record.createdAt instanceof Date); // true
console.log(record.createdAt.getFullYear()); // 2023Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.