Skip to main content

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, функцій, класів, циклічних посилань.

Швидкий приклад

javascript
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 змінює або відкидає

javascript
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 BigInt

replacer та reviver

Обидва методи мають необов'язковий параметр для нестандартних сценаріїв. replacer у stringify фільтрує або трансформує значення на виході. reviver у parse відновлює типи на вході.

javascript
// 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); // true

Replacer викликається зверху вниз від кореня; reviver - знизу вгору, спочатку вкладені властивості.

Як це працює у V8

У V8 JSON.stringify() використовує C++ серіалізатор, який обходить граф об'єктів, записує примітиви напряму і викликає toJSON() де він є. JSON.parse() - це рекурсивний descent-парсер, що перевіряє UTF-8 та синтаксис за RFC 8259. Обидва методи працюють у нативному коді. Тому вони швидші за будь-яку бібліотеку для JSON написану на JavaScript.

Типові помилки

1. Забути зробити parse після читання з localStorage

javascript
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 переживе серіалізацію

javascript
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); // true

3. Циклічні посилання ламають stringify

javascript
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()

javascript
JSON.parse({ a: 1 }); // SyntaxError // Об'єкт зводиться до рядка "[object Object]", який не є валідним JSON. // Передавай тільки рядок: JSON.parse('{"a":1}'); // { a: 1 }

5. Використовувати JSON для deep clone при наявності спеціальних типів

javascript
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: збереження та відновлення налаштувань

javascript
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

javascript
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 для нестандартних сценаріїв

javascript
// 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

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?