Пошук уроків, статей та іншого контенту
Навчитеся серіалізувати об’єкти й масиви через JSON та враховувати обмеження такого перетворення.
Серіалізація — це перетворення структури даних у формат, який можна зберегти або передати:
мережею;
у файл;
між процесами;
між різними мовами програмування.
JSON — текстовий формат, побудований на обмеженій множині значень:
об’єкти;
масиви;
рядки;
числа;
true;
false;
null.
У JavaScript для роботи з JSON використовують два основні методи:
JSON.stringify(value); // серіалізація
JSON.parse(text); // десеріалізаціяconst user = {
id: 42,
name: "Olena",
roles: ["admin", "editor"],
active: true
};
const json = JSON.stringify(user);
console.log(json);
// {"id":42,"name":"Olena","roles":["admin","editor"],"active":true}
const restoredUser = JSON.parse(json);
console.log(restoredUser.name);
// OlenaПісля JSON.stringify значення стає рядком. Після JSON.parse рядок перетворюється назад на JavaScript-структуру.
JSON.stringify може серіалізувати примітиви, масиви та звичайні об’єкти.
console.log(JSON.stringify("hello")); // "\"hello\""
console.log(JSON.stringify(123)); // "123"
console.log(JSON.stringify(true)); // "true"
console.log(JSON.stringify(null)); // "null"
console.log(JSON.stringify([1, "two", false]));
// [1,"two",false]
console.log(JSON.stringify({ answer: 42 }));
// {"answer":42}Назви властивостей об’єктів у JSON завжди стають рядками та беруться в подвійні лапки.
const object = {
1: "one",
true: "boolean key"
};
console.log(JSON.stringify(object));
// {"1":"one","true":"boolean key"}Порядок властивостей зазвичай відповідає порядку їхнього обходу в JavaScript, але не варто використовувати звичайний JSON як універсальний механізм для криптографічного або канонічного порівняння об’єктів.
Метод JSON.stringify має чотири аргументи:
JSON.stringify(value, replacer, space);Третій аргумент space задає форматування результату.
const data = {
user: {
id: 7,
name: "Ihor"
},
permissions: ["read", "write"]
};
console.log(JSON.stringify(data, null, 2));Результат:
{
"user": {
"id": 7,
"name": "Ihor"
},
"permissions": [
"read",
"write"
]
}Число 2 означає кількість пробілів для відступу. Також можна передати рядок, наприклад "\t".
Форматування збільшує розмір JSON, тому для мережевих запитів зазвичай використовують компактний варіант без відступів, а для логів і файлів конфігурації — форматований.
JSON не має окремих представлень для:
undefined;
функцій;
символів;
BigInt;
NaN;
Infinity;
-Infinity;
Date;
Map;
Set;
RegExp;
екземплярів довільних класів.
Це не означає, що всі такі значення завжди повністю зникають. Результат залежить від того, де вони розташовані.
undefined, функції та символиУ властивостях об’єкта значення undefined, функції та символи пропускаються:
const object = {
defined: 1,
missing: undefined,
method() {},
symbol: Symbol("id")
};
console.log(JSON.stringify(object));
// {"defined":1}У масиві такі значення перетворюються на null, щоб не змінювати індекси елементів:
const array = [
undefined,
function () {},
Symbol("value"),
10
];
console.log(JSON.stringify(array));
// [null,null,null,10]Якщо серіалізувати окремо undefined, функцію або символ, результатом буде undefined, а не JSON-рядок:
console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify(() => {})); // undefined
console.log(JSON.stringify(Symbol("id"))); // undefinedJSON підтримує числа, але не розрізняє звичайні числа та спеціальні значення NaN, Infinity, -Infinity.
Вони перетворюються на null:
const values = {
valid: 12.5,
notANumber: NaN,
positiveInfinity: Infinity,
negativeInfinity: -Infinity
};
console.log(JSON.stringify(values));
// {"valid":12.5,"notANumber":null,"positiveInfinity":null,"negativeInfinity":null}Якщо застосунку важливо відновити такі значення, їх потрібно явно кодувати перед серіалізацією.
BigIntСпроба напряму серіалізувати BigInt завершується помилкою:
const data = {
id: 9007199254740993n
};
// TypeError: Do not know how to serialize a BigInt
JSON.stringify(data);Безпечний підхід — перетворити BigInt на рядок:
const data = {
id: 9007199254740993n
};
const json = JSON.stringify(data, (key, value) => {
return typeof value === "bigint" ? `${value}n` : value;
});
console.log(json);
// {"id":"9007199254740993n"}Під час десеріалізації такий рядок можна відновити, якщо формат відомий:
const restored = JSON.parse(json, (key, value) => {
if (typeof value === "string" && /^\d+n$/.test(value)) {
return BigInt(value.slice(0, -1));
}
return value;
});
console.log(restored.id);
console.log(typeof restored.id);
// 9007199254740993n
// bigintУ реальному форматі краще використовувати явну структуру, наприклад:
{"id":{"type":"bigint","value":"9007199254740993"}}Так менший ризик випадково перетворити звичайний рядок на BigInt.
Об’єкт Date не зберігається як об’єкт дати. Перед серіалізацією він перетворюється на ISO-рядок.
const event = {
createdAt: new Date("2025-03-10T12:30:00.000Z")
};
const json = JSON.stringify(event);
console.log(json);
// {"createdAt":"2025-03-10T12:30:00.000Z"}
const restored = JSON.parse(json);
console.log(typeof restored.createdAt);
// stringJSON.parse не знає, що цей рядок колись був датою. Для відновлення потрібно зробити це явно:
const restored = JSON.parse(json, (key, value) => {
if (key === "createdAt") {
return new Date(value);
}
return value;
});
console.log(restored.createdAt instanceof Date);
// trueНадійніше перевіряти не лише ім’я властивості, а й формат значення. Інакше можна випадково перетворити звичайний текст із таким самим ім’ям.
const restored = JSON.parse(json, (key, value) => {
if (
typeof value === "string" &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
) {
return new Date(value);
}
return value;
});replacer: контроль серіалізаціїДругий аргумент JSON.stringify називається replacer. Він може бути:
функцією;
масивом дозволених ключів.
replacerФункція отримує ключ і значення. Для кореневого значення ключем буде порожній рядок.
const account = {
id: 10,
email: "user@example.com",
password: "secret",
token: "private-token",
profile: {
name: "Marta"
}
};
const json = JSON.stringify(account, (key, value) => {
if (key === "password" || key === "token") {
return undefined;
}
return value;
});
console.log(json);
// {"id":10,"email":"user@example.com","profile":{"name":"Marta"}}Повернення undefined означає:
для властивості об’єкта — не включати її;
для елемента масиву — записати null;
для кореневого значення — отримати undefined.
replacer зручно використовувати для:
видалення секретів;
перетворення нестандартних типів;
нормалізації даних;
обмеження розміру структури;
створення власного формату.
Наприклад, Map можна перетворити на масив пар:
const state = {
cache: new Map([
["theme", "dark"],
["language", "uk"]
])
};
const json = JSON.stringify(state, (key, value) => {
if (value instanceof Map) {
return {
type: "Map",
entries: [...value.entries()]
};
}
return value;
});
console.log(json);
// {"cache":{"type":"Map","entries":[["theme","dark"],["language","uk"]]}}Якщо другим аргументом передати масив рядків, JSON міститиме лише перелічені властивості.
const user = {
id: 1,
name: "Olena",
email: "olena@example.com",
password: "secret"
};
const json = JSON.stringify(user, ["id", "name", "email"]);
console.log(json);
// {"id":1,"name":"Olena","email":"olena@example.com"}Такий варіант діє рекурсивно для вкладених об’єктів. Проте для захисту секретів краще не покладатися лише на список дозволених полів: схему даних може бути змінено, а нове поле може випадково потрапити в інший кодовий шлях.
toJSONЯкщо об’єкт має метод toJSON, JSON.stringify використовує результат цього методу замість самого об’єкта.
class User {
constructor(id, name, password) {
this.id = id;
this.name = name;
this.password = password;
}
toJSON() {
return {
id: this.id,
name: this.name
};
}
}
const user = new User(1, "Sofiia", "secret");
console.log(JSON.stringify(user));
// {"id":1,"name":"Sofiia"}toJSON також викликається у Date. Саме тому дата стає ISO-рядком.
Метод toJSON корисний, коли клас має власне публічне представлення. Але він впливає на кожен виклик JSON.stringify для цього об’єкта. Якщо в різних контекстах потрібні різні формати, часто краще використовувати окремий серіалізатор.
reviver: контроль десеріалізаціїДругий аргумент JSON.parse — функція reviver:
JSON.parse(text, reviver);Вона викликається для кожної властивості після розбору JSON. Значення, яке повертає функція, замінює поточне. Якщо повернути undefined, властивість буде видалено.
const json = `{
"id": 7,
"name": "Andrii",
"active": true,
"temporary": "remove"
}`;
const user = JSON.parse(json, (key, value) => {
if (key === "temporary") {
return undefined;
}
return value;
});
console.log(user);
// { id: 7, name: "Andrii", active: true }Для відновлення типів можна використовувати маркери:
const json = `{
"createdAt": {
"__type": "Date",
"value": "2025-01-15T10:00:00.000Z"
}
}`;
const data = JSON.parse(json, (key, value) => {
if (value && value.__type === "Date") {
return new Date(value.value);
}
return value;
});
console.log(data.createdAt instanceof Date);
// truereviver не є механізмом валідації схеми. Він лише перетворює вже розібрану структуру. Обов’язкові поля, типи та діапазони потрібно перевіряти окремо.
JSON описує дерево даних. Об’єкт не може напряму посилатися сам на себе або створювати цикл.
const node = {
name: "root"
};
node.self = node;
JSON.stringify(node);
// TypeError: Converting circular structure to JSONЦикл може бути непрямим:
const parent = {};
const child = { parent };
parent.child = child;
JSON.stringify(parent);
// TypeError: Converting circular structure to JSONПеред серіалізацією потрібно або прибрати цикли, або перетворити граф на спеціальний формат із ідентифікаторами.
Простий replacer може пропускати повторні посилання:
const root = {
name: "root",
child: {
name: "child"
}
};
root.child.parent = root;
const seen = new WeakSet();
const json = JSON.stringify(root, (key, value) => {
if (value && typeof value === "object") {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
}
return value;
});
console.log(json);Такий підхід підходить для логування, але не для повного відновлення структури: після десеріалізації рядок "[Circular]" не стане посиланням на початковий об’єкт.
Для складних графів потрібно серіалізувати вузли окремо:
const graph = {
nodes: [
{ id: "a", value: "root" },
{ id: "b", value: "child" }
],
edges: [
{ from: "a", to: "b" },
{ from: "b", to: "a" }
]
};Тут зв’язки представлені ідентифікаторами, тому JSON може їх зберегти без циклічних посилань.
Після JSON.parse результатом є звичайні об’єкти, а не екземпляри початкових класів.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
length() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
const point = new Point(3, 4);
const restored = JSON.parse(JSON.stringify(point));
console.log(restored instanceof Point);
// false
console.log(typeof restored.length);
// undefinedJSON зберігає дані, але не:
прототип;
методи;
конструктор;
приватні поля;
дескриптори властивостей;
Map або Set як відповідні екземпляри.
Якщо потрібно відновити клас, слід передати тип і створити екземпляр у reviver:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
length() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
const json = JSON.stringify({
type: "Point",
x: 3,
y: 4
});
const restored = JSON.parse(json, (key, value) => {
if (value && value.type === "Point") {
return new Point(value.x, value.y);
}
return value;
});
console.log(restored instanceof Point);
// true
console.log(restored.length());
// 5Не слід автоматично створювати екземпляри класів на основі довільного значення, отриманого від користувача. Це може призвести до небезпечної логіки десеріалізації. Типи повинні бути явно дозволені, а вхідні дані — перевірені.
JavaScript зберігає числа як Number, тому великі цілі значення можуть втратити точність ще до серіалізації.
const value = 9007199254740993;
console.log(value);
// 9007199254740992Максимальне безпечно представлене ціле число можна отримати через Number.MAX_SAFE_INTEGER.
console.log(Number.MAX_SAFE_INTEGER);
// 9007199254740991JSON не вирішує цю проблему. Якщо ідентифікатор або грошове значення може перевищувати безпечний діапазон, передавайте його як рядок:
const response = {
id: "9007199254740993",
amount: "12345678901234567890"
};
const json = JSON.stringify(response);
const data = JSON.parse(json);
console.log(data.id);
// 9007199254740993Для грошей рядкове представлення часто краще за number, оскільки десяткові дроби також можуть мати похибку двійкового представлення.
JSON.parse приймає лише коректний JSON. Некоректний рядок спричиняє SyntaxError.
try {
const data = JSON.parse('{"id": 1}');
console.log(data);
} catch (error) {
console.error("Некоректний JSON:", error.message);
}На межі системи — наприклад, під час отримання HTTP-відповіді або читання файлу — потрібно обробляти помилки розбору.
Однак успішний JSON.parse не означає, що дані відповідають очікуваній структурі:
const data = JSON.parse('{"id":"not-a-number"}');
console.log(data.id);
// not-a-numberПісля розбору слід виконувати валідацію:
function parseUser(json) {
const value = JSON.parse(json);
if (
!value ||
typeof value !== "object" ||
!Number.isInteger(value.id) ||
typeof value.name !== "string"
) {
throw new TypeError("Некоректна структура користувача");
}
return {
id: value.id,
name: value.name
};
}
const user = parseUser('{"id": 5, "name": "Nazar"}');
console.log(user);
// { id: 5, name: "Nazar" }Не слід виконувати JSON через eval. JSON.parse розбирає формат даних без виконання коду.
Для обміну складними структурами корисно заздалегідь визначити формат. Наприклад, можна описати Set, Map, дату та BigInt через явні маркери.
function serialize(value) {
return JSON.stringify(value, (key, currentValue) => {
if (typeof currentValue === "bigint") {
return {
__type: "BigInt",
value: currentValue.toString()
};
}
if (currentValue instanceof Date) {
return {
__type: "Date",
value: currentValue.toISOString()
};
}
if (currentValue instanceof Set) {
return {
__type: "Set",
value: [...currentValue]
};
}
if (currentValue instanceof Map) {
return {
__type: "Map",
value: [...currentValue.entries()]
};
}
return currentValue;
});
}
function deserialize(json) {
return JSON.parse(json, (key, currentValue) => {
if (!currentValue || typeof currentValue !== "object") {
return currentValue;
}
switch (currentValue.__type) {
case "BigInt":
return BigInt(currentValue.value);
case "Date":
return new Date(currentValue.value);
case "Set":
return new Set(currentValue.value);
case "Map":
return new Map(currentValue.value);
default:
return currentValue;
}
});
}
const original = {
id: 9007199254740993n,
createdAt: new Date("2025-05-01T12:00:00.000Z"),
tags: new Set(["javascript", "json"]),
settings: new Map([
["theme", "dark"],
["language", "uk"]
])
};
const json = serialize(original);
const restored = deserialize(json);
console.log(json);
console.log(typeof restored.id);
// bigint
console.log(restored.createdAt instanceof Date);
// true
console.log(restored.tags instanceof Set);
// true
console.log(restored.settings instanceof Map);
// trueУ цьому прикладі є важлива деталь: Date має власний метод toJSON, тому в загальному випадку JSON.stringify може передати replacer уже як рядок ISO, а не як об’єкт Date. Якщо потрібно гарантовано кодувати дати власним форматом, їх потрібно обробити до виклику JSON.stringify або використовувати інший підхід до нормалізації даних.
Практичний серіалізатор повинен також:
обмежувати дозволені типи;
перевіряти розмір вхідних даних;
не відновлювати довільні класи;
обробляти цикли;
мати версію формату;
зберігати сумісність під час змін структури.
Конструкцію часто використовують для простого копіювання:
const copy = JSON.parse(JSON.stringify(original));Але це не універсальне глибоке копіювання. Під час такого перетворення:
втрачаються методи;
втрачаються прототипи;
Date стає рядком;
Map і Set втрачають тип;
undefined, функції та символи зникають або стають null;
BigInt спричиняє помилку;
циклічні посилання не підтримуються;
спеціальні числові значення стають null;
можуть втрачатися посилання між однаковими об’єктами.
Для даних, які справді відповідають JSON-моделі, такий підхід може бути достатнім. Для довільних JavaScript-структур він небезпечний, оскільки копія може непомітно відрізнятися від оригіналу.
Іноді JSON використовують для порівняння:
const first = { a: 1, b: 2 };
const second = { a: 1, b: 2 };
console.log(JSON.stringify(first) === JSON.stringify(second));
// trueЦе працює лише для обмежених випадків. Результат залежить від:
порядку властивостей;
втрати типів;
поведінки undefined;
представлення чисел;
циклічних посилань;
власного toJSON.
const first = { a: 1, b: 2 };
const second = { b: 2, a: 1 };
console.log(JSON.stringify(first) === JSON.stringify(second));
// може бути falseТому JSON-порівняння не є загальним алгоритмом перевірки структурної рівності.
const original = {
createdAt: new Date()
};
const restored = JSON.parse(JSON.stringify(original));
console.log(restored.createdAt instanceof Date);
// falseJSON зберігає текстове представлення, а не JavaScript-прототип.
Перед логуванням або відправленням об’єкта перевіряйте, чи немає в ньому:
паролів;
токенів;
ключів доступу;
cookie;
персональних даних;
внутрішніх службових полів.
Найкраще формувати DTO або використовувати явний список дозволених полів, а не серіалізувати весь об’єкт запиту чи моделі.
undefinedconst payload = {
name: "Olena",
phone: undefined
};
console.log(JSON.stringify(payload));
// {"name":"Olena"}Відсутнє поле та поле зі значенням undefined можуть мати різну семантику в API. Якщо потрібно передати саме «значення відсутнє», використовуйте null або окремий формат операції.
reviverreviver може змінити або видалити будь-які властивості. Не використовуйте його для автоматичного створення довільних класів чи виконання логіки на основі неперевірених імен типів.
Великі JSON-структури:
займають додаткову пам’ять;
потребують часу на серіалізацію та розбір;
можуть блокувати головний потік у браузері;
збільшують мережевий трафік.
Для великих даних варто використовувати пагінацію, потокову обробку, компактні формати або передавати лише потрібні поля.
У наступному прикладі внутрішня модель користувача відокремлена від даних, які можна передати клієнту:
class User {
constructor({ id, email, name, passwordHash, roles }) {
this.id = id;
this.email = email;
this.name = name;
this.passwordHash = passwordHash;
this.roles = roles;
}
toPublicDto() {
return {
id: this.id,
email: this.email,
name: this.name,
roles: [...this.roles]
};
}
}
function serializePublicUser(user) {
const dto = user.toPublicDto();
return JSON.stringify(dto);
}
const user = new User({
id: 15,
email: "user@example.com",
name: "Kateryna",
passwordHash: "internal-hash",
roles: new Set(["reader", "author"])
});
const json = serializePublicUser(user);
console.log(json);
// {"id":15,"email":"user@example.com","name":"Kateryna","roles":["reader","author"]}Тут серіалізується не весь екземпляр, а спеціально підготовлений DTO. Це зменшує ризик витоку внутрішніх властивостей і робить формат API явним.
Вважати, що JSON.stringify зберігає класи, методи та прототипи.
Очікувати, що після JSON.parse рядок дати автоматично стане Date.
Передавати BigInt без попереднього перетворення.
Використовувати JSON як копіювальний механізм для структур із Map, Set, циклами або спеціальними типами.
Порівнювати об’єкти лише через JSON.stringify.
Забувати, що undefined у властивості об’єкта зникає.
Не перевіряти JSON після розбору на відповідність схемі.
Серіалізувати весь внутрішній об’єкт разом із паролями та токенами.
Автоматично створювати класи у reviver на основі неперевірених даних.
Ігнорувати обмеження точності Number для великих ідентифікаторів.
Не враховувати циклічні посилання.
Використовувати форматований JSON у місцях, де важливі мінімальний розмір і продуктивність.
JSON.stringify перетворює JavaScript-значення на JSON-рядок.
JSON.parse перетворює коректний JSON-рядок назад на JavaScript-структуру.
JSON підтримує лише обмежену множину типів і не зберігає прототипи, методи та довільні класи.
replacer дає змогу контролювати серіалізацію.
reviver дає змогу відновлювати спеціальні типи після розбору.
Date серіалізується як ISO-рядок, а BigInt потребує явного кодування.
undefined, функції та символи можуть зникнути або перетворитися на null.
Циклічні посилання не можна напряму серіалізувати в JSON.
Для зовнішніх даних потрібні валідація, обмеження дозволених полів і контроль секретів.
Для складних структур слід визначати явний версійований формат серіалізації.