Сериализация: JSON, V8, BSON

Преобразование объектов в строку/байты для передачи и хранения. JSON универсален, но не сохраняет классы, функции, символы, Date. Бинарные форматы (BSON, v8) плотнее и сохраняют больше.

Что это

Сериализация — превращение объекта в памяти (структура с указателями) в линейный формат, который можно сохранить или передать. Десериализация — обратное. Делятся на текстовые (CSV, JSON, XML, YAML — читаемые) и бинарные (BSON, v8.serialize, protobuf — плотные, нечитаемые).

В JSON у каждого числа разная длина (1 vs 100), в бинарном — одинаковая. Бинарный формат компактнее в одном, тяжелее в другом.

JSON и его ограничения

JSON.stringify({ a: 1, b: undefined, c: Infinity, d: NaN, e: null });
// '{"a":1,"c":null,"d":null,"e":null}'
// undefined пропадает, Infinity и NaN становятся null

Что теряется при JSON.stringify:

  • undefined — выпадает из ключей и массивов
  • Symbol — игнорируется
  • Infinity, NaN — становятся null
  • Date — превращается в строку через toString, обратно в Date сам не парсится
  • Классы: new A и new B сериализуются одинаково — информации о классе нет
  • Функции — пропадают

Бинарная сериализация V8

const v8 = require('node:v8');

const obj = { date: new Date, big: 9999n, set: new Set([1, 2, 3]) };
const buf = v8.serialize(obj);   // Buffer
const back = v8.deserialize(buf); // объект восстановлен

// v8 сохраняет: Date, BigInt, Map, Set, RegExp, TypedArray

v8.serialize использует внутренний формат V8 (HostObject protocol) — это байндинг к встроенным библиотекам. Не путать с vmvm.createScript исполняет JS-код, а v8 сериализует значения.

Свой расширяемый сериализатор

// Коллекция: тип → функция сериализации
const serializers = {
  null:  => 'null',
  string: (v) => JSON.stringify(v),
  number: (v) => v.toString(),
  boolean: (v) => v.toString(),
  function: (v) => v.toString(), // toString функции возвращает её код
  array: (v) => `[${v.map(serialize).join(',')}]`,
  object: (v) => {
    const pairs = Object.entries(v).map(([k, val]) => `${k}:${serialize(val)}`);
    const symbols = Object.getOwnPropertySymbols(v)
      .map(s => `${s.toString()}:${serialize(v[s])}`);
    return `{${[...pairs, ...symbols].join(',')}}`;
  },
};

function serialize(obj) {
  if (obj === null) return serializers.null;
  const type = Array.isArray(obj) ? 'array' : typeof obj;
  return serializers[type](obj);
}

// Десериализация — через vm.createScript (свой формат — это валидный JavaScript)
const vm = require('node:vm');
const live = new vm.Script(`(${serialized})`).runInThisContext;

Подводные камни

  • JSON не для бизнес-логики с типами: Date, классы, Map/Set теряются — нужны DTO с явным парсингом
  • JSON.stringify(circular)TypeError. Нужен util.inspect или собственный обход с WeakSet
  • CSV не split-ом: внутри ячейки могут быть кавычки и запятые с escape — простой split ломается
  • v8.serialize не кроссплатформенный между мажорными версиями V8 — не для долговременного хранилища

🎓 Источники

  • 🎓 Сериализация и десериализация в JavaScript и Node.js · 2018-11-26
    • Тезисы:
      • В JSON 1 — длина 1, 100 — длина 3. В бинарном — одинаковая длина. Уплотняет в одном, увеличивает в другом
      • console.dir тоже сериализатор: для вывода объекта нужно превратить указатель в строку
      • Свой формат может сериализовать функции через function.toString() (для native — пометка native code)
      • Расширяемый сериализатор: справочник «тип → функция сериализации»; главная функция становится маленькой
      • Десериализация своего формата через vm.createScript: формат — это валидный JS, можем исполнить
      • v8.serialize/deserialize — байндинг на встроенные библиотеки V8 (не путать с vm)
      • Для символов нужен отдельный цикл по Object.getOwnPropertySymbols
    • Цитата:

      «Тут получается непрямая рекурсия, потому что внутри каждого сериализаторов может опять лежать serialize. Две функции, одна другую вызывает.»

См. также