Перейти к содержимому
Toova
Все инструменты

7 приёмов JSON, которые сэкономят вам часы

Toova

Каждый JavaScript-разработчик использует JSON.parse и JSON.stringify десятки раз в неделю. Но большинство останавливается на базовом — разборе API-ответов и сериализации объектов в строки. У API гораздо больше возможностей, и некоторые из менее известных функций решают задачи, которые иначе потребовали бы сторонних библиотек или часов отладки.

Это руководство охватывает семь приёмов, выходящих за рамки стандартных. Каждый из них немедленно применим к реальным кодовым базам. Без абстракций, без надуманных примеров — только шаблоны, встречающиеся в production-системах.

Проверить и изучить любой пример из этой статьи можно в Toova JSON Formatter, который валидирует и форматирует JSON прямо в браузере.

1. Глубокое клонирование через JSON-цикл (и его ограничения)

Старейший приём в арсенале JavaScript: используйте JSON.parse(JSON.stringify(obj)) для создания глубокой копии простого объекта.

const original = { a: 1, b: { c: 2 } };

// Наивный подход — вложенные объекты всё ещё разделяются
const shallowCopy = { ...original }; // b по-прежнему указывает на тот же объект

// JSON-клонирование — создаёт полностью независимую копию
const deepClone = JSON.parse(JSON.stringify(original));

deepClone.b.c = 99;
console.log(original.b.c); // 2 — оригинал не тронут

Это работает, потому что сериализация в строку и последующий разбор создают полностью новое дерево объектов без общих ссылок. Быстро, не требует зависимостей и доступно начиная с ES5.

Современная альтернатива для использования в памяти — structuredClone():

// Современная альтернатива: structuredClone() — обрабатывает больше типов
// Поддерживается в Node.js 17+ и всех вечнозелёных браузерах
const clone = structuredClone(original);

Знайте ограничения JSON-подхода, прежде чем на него полагаться:

// JSON.parse/stringify НЕ МОЖЕТ обрабатывать:
const broken = {
  date: new Date(),      // становится строкой — теряет прототип Date
  fn: () => 'hello',    // молча удаляется
  undef: undefined,     // молча удаляется
  inf: Infinity,        // становится null
  map: new Map(),       // становится {}
  cycle: null,          // циклические ссылки вызывают ошибку
};

Если объект содержит только простые данные — строки, числа, булевы значения, массивы и вложенные простые объекты — JSON-цикл безопасен и быстр. Для всего остального предпочтите structuredClone() или специализированную библиотеку.

2. Кастомный replacer для фильтрации и маскировки

Большинство разработчиков знают, что JSON.stringify принимает второй аргумент, но редко его используют. Этот второй аргумент — replacer: либо массив включаемых ключей, либо функция, точно управляющая сериализацией каждого значения.

Массив-replacer — белый список конкретных ключей:

const user = {
  id: 'u_001',
  name: 'Alice',
  password: 'hunter2',        // не должно попасть в логи
  creditCard: '4111111111111111', // не должно попасть в логи
  role: 'admin',
};

// Replacer как массив: включать только эти ключи
JSON.stringify(user, ['id', 'name', 'role']);
// '{"id":"u_001","name":"Alice","role":"admin"}'

Функция-replacer — преобразование или скрытие значений:

// Replacer как функция: полный контроль над ключом/значением
const masked = JSON.stringify(user, (key, value) => {
  if (key === 'password' || key === 'creditCard') return '[REDACTED]';
  if (key === '' ) return value; // корневой объект — всегда возвращаем
  return value;
});
// '{"id":"u_001","name":"Alice","password":"[REDACTED]","creditCard":"[REDACTED]","role":"admin"}'

Более сложная версия маскирует значения на основе их формы, а не имени ключа:

// Replacer для маскировки на основе типа значения
const sanitize = (key, value) => {
  if (typeof value === 'string' && value.match(/^4[0-9]{15}$/)) {
    return '****-****-****-' + value.slice(-4);
  }
  return value;
};

Этот приём незаменим для middleware логирования: нужны структурированные логи с полным контекстом объекта, но определённые поля никогда не должны попасть в агрегатор логов. Replacer позволяет обработать это на границе сериализации, а не разбрасывать логику скрытия по всей кодовой базе.

3. Детерминированная сортировка ключей

Порядок ключей объекта в JavaScript — порядок добавления (для строковых ключей). Два объекта с одинаковыми ключами, созданные в разном порядке, дают разные JSON-строки, что ломает наивные проверки равенства, ключи кэша и хеши содержимого.

function sortedStringify(obj) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

const a = { z: 1, a: 2, m: 3 };
const b = { a: 2, m: 3, z: 1 };

sortedStringify(a) === sortedStringify(b); // true — порядок ключей нормализован

Для глубоко вложенных объектов применяйте сортировку рекурсивно:

// Рекурсивная сортировка ключей для вложенных объектов
function sortKeys(value) {
  if (Array.isArray(value)) return value.map(sortKeys);
  if (value !== null && typeof value === 'object') {
    return Object.fromEntries(
      Object.keys(value).sort().map((k) => [k, sortKeys(value[k])])
    );
  }
  return value;
}

const sorted = JSON.stringify(sortKeys(deepNested));

Отсортированный JSON необходим, когда вы:

  • Генерируете ключи кэша из тел запросов
  • Вычисляете контрольные суммы или подписи JSON-нагрузок
  • Сравниваете API-ответы в тестах независимо от порядка полей
  • Храните конфигурационные объекты, где порядок не должен влиять на равенство

После сортировки и форматирования используйте инструмент Text Diff, чтобы сравнить две нормализованные JSON-строки и точно увидеть, какие значения изменились между версиями.

4. Работа с BigInt без потери точности

Тип Number в JavaScript безопасно представляет целые числа до 253 − 1. Для идентификаторов, генерируемых распределёнными системами, финансовых сумм в минорных единицах или временных меток в наносекундах этого недостаточно. BigInt покрывает целые числа произвольной точности, но JSON.stringify не умеет их обрабатывать.

const data = {
  amount: 9007199254740993n, // больше Number.MAX_SAFE_INTEGER
};

// Это вызывает ошибку: TypeError: Do not know how to serialize a BigInt
JSON.stringify(data); // ОШИБКА

Стандартные обходные пути:

// Решение 1: Преобразовать в строку через replacer
JSON.stringify(data, (key, value) =>
  typeof value === 'bigint' ? value.toString() : value
);
// '{"amount":"9007199254740993"}'

// Решение 2: toJSON() на прототипе BigInt (monkey-patch — использовать осторожно)
BigInt.prototype.toJSON = function () { return this.toString(); };
JSON.stringify(data); // '{"amount":"9007199254740993"}'

На стороне разбора функция reviver может восстанавливать значения BigInt из их строкового представления:

// Reviver для восстановления BigInt при разборе
const revived = JSON.parse('{"amount":"9007199254740993"}', (key, value) => {
  if (key === 'amount') return BigInt(value);
  return value;
});
console.log(typeof revived.amount); // 'bigint'

Держите преобразование BigInt в строку в общем слое сериализации, чтобы оно применялось последовательно. Если BigInt «утекают» в случайные вызовы JSON.stringify на границах кодовой базы, это приводит к непредсказуемым ошибкам, которые трудно отследить.

5. Обнаружение циклических ссылок

Циклическая ссылка возникает, когда объект содержит ссылку на себя или на предка в графе объектов. Они встречаются чаще, чем можно подумать: эмиттеры событий, DOM-узлы, fiber-узлы React и сущности ORM часто имеют обратные ссылки.

// Пример циклической ссылки
const obj = { name: 'node' };
obj.self = obj; // obj ссылается на себя

JSON.stringify(obj); // вызывает ошибку: TypeError: Converting circular structure to JSON

Обработайте это с помощью кастомного replacer, отслеживающего посещённые объекты:

// Обработчик циклических ссылок вручную
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

safeStringify(obj); // '{"name":"node","self":"[Circular]"}'

WeakSet — правильная структура данных здесь: она хранит ссылки на объекты, не препятствуя сборке мусора, а поиск занимает O(1). Этот шаблон также работает в middleware логирования, где нужно, чтобы ошибки деградировали плавно, а не выбрасывали исключение при сериализации.

Распространённый вариант: вместо маркировки циклических ссылок как [Circular] заменять их строкой пути вроде [Circular: $.config.parent], чтобы явно указывать местоположение ссылки при отладке.

6. Красивый вывод массивов в одну строку

Третий аргумент JSON.stringify — отступ. Передача 2 или 4 раскрывает всё на несколько строк — отлично для объектов, но многословно для массивов примитивов вроде тегов, идентификаторов или координат.

const mixed = {
  title: 'Report',
  tags: ['json', 'api', 'debug'],
  config: { indent: 2, sortKeys: true },
  count: 42,
};

// По умолчанию: всё многострочно
JSON.stringify(mixed, null, 2);
// {
//   "title": "Report",
//   "tags": [
//     "json",
//     "api",
//     "debug"
//   ],
//   "config": {
//     "indent": 2,
//     "sortKeys": true
//   },
//   "count": 42
// }

Можно обработать вывод постфактум, чтобы свернуть простые массивы в одну строку:

// Кастомный replacer для массивов в одну строку
function prettyMixed(obj) {
  const raw = JSON.stringify(obj, null, 2);
  // Сворачиваем массивы, содержащие только примитивы, в одну строку
  return raw.replace(
    /\[\n\s+([\s\S]*?)\n\s+\]/g,
    (match, inner) => {
      const items = inner.split(',\n').map((s) => s.trim());
      if (items.every((s) => !/^[{\[]/.test(s))) {
        return '[' + items.join(', ') + ']';
      }
      return match;
    }
  );
}

Результат: объекты остаются многострочными для читаемости, массивы примитивов сворачиваются для компактности. Такой формат используется во многих конфигурационных файлах и структурированных выводах логов, где данные должны читаться и людьми, и машинами. JSON Formatter и конвертер JSON в YAML выполняют это сворачивание автоматически.

7. Потоковая обработка большого JSON

Полная загрузка большого JSON-файла в память перед разбором — самая частая ошибка производительности в пайплайнах обработки JSON. Ответ размером 100 МБ выделяет не менее 100 МБ кучи для сырой строки, затем второе выделение для разобранного объекта. Для файлов объёмом выше нескольких мегабайт потоковый парсер обрабатывает данные постепенно.

В Node.js с потоковой библиотекой:

// node --experimental-vm-modules (или используйте потоковую JSON-библиотеку)
// Нативная потоковая обработка с JSON Source Map proposal (Stage 3, 2026):

// Пока используйте библиотеку @streamparser/json или jsonstream2
import { createReadStream } from 'fs';
import parser from '@streamparser/json';

const jsonParser = new parser.JSONParser();
jsonParser.onValue = ({ value, key, parent }) => {
  if (key === 'id') {
    console.log('Найден id:', value);
  }
};

createReadStream('large.json').pipe(jsonParser);

В браузере через Fetch Streams API:

// Потоковая обработка в браузере через Fetch + JSON stream decoder:
const response = await fetch('/api/large-data');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  // Обрабатываем полные JSON-объекты из буфера...
}

Потоковая обработка наиболее полезна в батч-пайплайнах, крупных экспортных эндпоинтах и скриптах анализа логов. Для обычных API-ответов до 1 МБ стандартный JSON.parse достаточно быстр и значительно проще. Порог, где потоковая обработка начинает реально окупаться, — около 5–10 МБ, в зависимости от сложности разбираемой структуры и размера кучи целевой среды.

Чтобы изучить структуру больших JSON-ответов перед построением потокового парсера, вставьте образец в JSON Formatter и посмотрите, какие пути содержат нужные данные, затем конвертируйте в CSV или YAML для дальнейшего анализа.

Всё вместе

Эти семь приёмов охватывают наиболее значимые возможности JSON API:

  • Глубокое клонирование через JSON-цикл — быстро для простых данных, знайте ограничения
  • Replacer — фильтрация ключей и маскировка чувствительных значений на границе сериализации
  • Сортировка ключей — детерминированный вывод для ключей кэша, подписей и утверждений в тестах
  • Работа с BigInt — сериализуем в строку, восстанавливаем при разборе
  • Обнаружение циклических ссылок — replacer на основе WeakSet для безопасной сериализации
  • Красивый вывод с компактными массивами — человекочитаемый вывод без лишних пробелов
  • Потоковый JSON — инкрементальный разбор для больших файлов и API-ответов

Полная документация по аргументам replacer и space функции JSON.stringify есть в справочнике MDN JSON.stringify. Для современной альтернативы глубокому клонированию смотрите документацию structuredClone() с описанием всех поддерживаемых типов и граничных случаев.