Изображение, созданное DALL-E
Изображение, созданное DALL-E

Ни для кого не секрет, что JSON широко используется в веб-разработке: обмен данными между клиентом (браузером) и сервером, хранение в NoSQL-базах, конфигурационные файлы, API-ответы и многое другое. Он стал практически родным форматом данных для JavaScript и Node.js. Однако при работе с JSON стоит учитывать ряд ограничений и подводных камней, которые в больших проектах могут вылиться в серьёзные проблемы с производительностью, точностью и безопасностью.

В этой статье мы разберём:

  1. Неочевидные проблемы при сериализации/десериализации JSON - с фокусом на веб-разработку.

  2. Обработку больших JSON-файлов - нюансы и инструменты в Node.js (и не только).

  3. Популярные альтернативы JSON: MessagePack и Protocol Buffers - когда и как их стоит применять в веб-приложениях.

Статья рассчитана на веб-разработчиков, которые работают с JSON каждый день и хотят глубже разобраться в его особенностях, а также расширить свой стек инструментов.

Неочевидные проблемы при сериализации и десериализации JSON (веб-контекст)

Большие числа и потеря точности

В JavaScript (и, соответственно, в браузере и Node.js) максимальное безопасное целое число равно 2^53 - 1. Если вы храните ID или денежные суммы, которые превышают этот порог, то при парсинге JSON может произойти потеря точности.

const jsonString = '{"order_id": 1234567890123456789, "price": 1499.95}';
const data = JSON.parse(jsonString);
console.log(data.order_id); 
// 1234567890123456800 — ошибка, хвост числа "округлился"

Рекомендации:

  • Храните слишком большие целые числа в JSON как строки: {"order_id": "1234567890123456789"}.

  • В Node.js (начиная с версии 10.4, а также в современных браузерах) можно использовать BigInt для точных вычислений: BigInt("1234567890123456789"). Но в JSON по-прежнему придётся обрабатывать как строку.

  • Если нужно передавать огромные суммы/балансы в финансовом контексте, рассмотрите передачу в строчном формате либо используйте специализированные решения (двоичные протоколы, см. раздел про ProtoBuf).

Даты и время

Стандарт JSON не содержит встроенного типа даты/времени. В веб-среде чаще всего встречаются три подхода:

  1. ISO 8601: 2025-01-04T12:34:56Z

  2. Unix Timestamp (в секундах или миллисекундах): 1672822561000

  3. Пользовательские форматы: 04/01/2025 12:34:56, 2025.01.04 12:34:56 и т.д.

Проблемы возникают, когда мы забываем учитывать:

  • Браузер хранит дату внутри объекта Date в формате UTC, но при выводе преобразует её в локальный часовой пояс, что может вызывать путаницу при обработке времени.

  • Несогласованность форматов. Например, сервер выдаёт "2025-01-04T12:34:56Z", а кто-то пытается парсить его как MM/DD/YYYY.

  • Некоторые библиотеки или операции могут обрезать миллисекунды из даты, что приводит к расхождению данных между различными системами.

Как решать:

  • Соблюдайте единый формат для дат, чаще всего используют ISO 8601 (UTC).

  • На клиенте используйте проверенные библиотеки: date-fns, Moment.js (находится в режиме поддержания и не развивается ), Day.js, Luxon.

    • Также обратите внимание на новый Temporal API (пока в стадии черновика), который потенциально может заменить Date в JavaScript.

  • На сервере (Node.js) для хранения и обработки дат в базе данных (например, PostgreSQL, MongoDB) старайтесь приводить всё к UTC.

Экранирование спецсимволов, Unicode и эмодзи

JSON-строки должны экранировать спецсимволы (\n, \t, \", \\). Если нужно передавать эмодзи или символы за пределами U+FFFF, то фактически они превращаются в суррогатные пары (например, \ud83d\ude00 для ?).

В большинстве случаев это прозрачно для веб-разработчика, но могут возникнуть проблемы:

  • Если JSON-строка несколько раз проходит через процесс сериализации, каждый этап добавляет обратные слеши, что может привести к накоплению экранированных символов и, в конечном итоге, к сложночитаемому "лесу" слешей.

  • Неверная работа с Unicode на этапах парсинга сторонними библиотеками или плагинами.

const data = {
  text: "Hello\nNewLine",
  emoji: "?"
};

const str = JSON.stringify(data);
console.log(str);
// {"text":"Hello\nNewLine","emoji":"\ud83d\ude00"}

const parsed = JSON.parse(str);
console.log(parsed);
// { text: 'Hello\nNewLine', emoji: '?' }

Обычно всё хорошо, если использовать стандартный JSON.parse/JSON.stringify, но если у вас сложный пайплайн (например, преобразование на стороне клиентских библиотек, несколько слоёв API), стоит убедиться, что все компоненты корректно обрабатывают экранирование.

Производительность

Веб-разработчики, создавая REST API, часто генерируют JSON-ответы на десятки мегабайт, а потом удивляются долгому парсингу и высокому расходу памяти.

  1. На стороне клиента: JSON.parse крупного ответа (особенно в мобильном браузере) может подвесить UI на несколько секунд.

  2. На стороне сервера (Node.js): JSON.stringify очень большого объекта тоже затратен.

Если ваше веб-приложение возвращает огромные JSONы:

  • Подумайте, действительно ли нужно отдавать всё сразу. Возможно, лучше сделать пагинацию, lazy-load или частичные данные.

  • Используйте стриминг там, где это уместно (Node.js stream + JSON chunking).

  • Сжимайте ответ (например, gzip или brotli в Express через compression).

Обработка больших JSON-файлов на Node.js

Стриминг и потоки (Streams API)

В Node.js крайне желательно обрабатывать большие JSON-файлы (или потоки данных) поэтапно, а не загружать их полностью в память.

  • SAX-подобные парсеры для JSON: stream-json, JSONStream.

  • Чтение файл → парсер → обработка: вы считываете файл через fs.createReadStream, передаёте в стриминговый парсер, который эмитит объекты по мере чтения.

Пример (используя stream-json):

const fs = require('fs');
const { parser } = require('stream-json');
const { streamValues } = require('stream-json/streamers/StreamValues');

const readStream = fs.createReadStream('big.json');

readStream
  .pipe(parser())
  .pipe(streamValues())
  .on('data', (data) => {
    // data.value содержит очередной кусок JSON
    console.log(data.value);
  })
  .on('end', () => {
    console.log('Done processing large JSON!');
  });

Таким образом, Node.js не хранит весь JSON в памяти, а обрабатывает по частям.

NDJSON / JSON Lines

Если у вас много однотипных объектов, рассмотрите формат NDJSON (Newline-Delimited JSON). Каждая строка - отдельный JSON-объект:

{"id":1,"name":"Item1"}
{"id":2,"name":"Item2"}
{"id":3,"name":"Item3"}

Читать такой файл через стримы в Node.js очень удобно. Можно построчно обрабатывать:

const fs = require('fs');
const readline = require('readline');

async function processNDJSON(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({ input: fileStream });

  for await (const line of rl) {
    const obj = JSON.parse(line);
    // Обработка obj
    console.log(obj.name);
  }
}

processNDJSON('items.ndjson');

Преимущества:

  • Не надо парсить огромный массив, можно сразу по строкам.

  • Если данные идут в реальном времени (например, лог-сервис), то NDJSON позволяет обрабатывать их на лету.

Компрессия

Для экономии места и ускорения передачи больших JSON-ответов по HTTP в продакшене практически всегда включают gzip или brotli-сжатие:

const express = require('express');
const compression = require('compression');

const app = express();
app.use(compression()); // Включаем сжатие

app.get('/api/data', (req, res) => {
  const bigObject = generateHugeObject();
  res.json(bigObject);
});

app.listen(3000, () => {
  console.log('Server running...');
});

На клиенте браузер автоматически декомпрессирует ответ, остаётся лишь распарсить JSON. Если же ваш сервис передаёт большие JSON-файлы другой системе, убедитесь, что другая сторона тоже умеет декомпрессировать (обычно это стандарт).

Альтернативы JSON: MessagePack и Protocol Buffers для веб

Это двоичный формат, который сохраняет структуру данных, похожую на JSON (объекты, массивы, строки, числа), но в более компактном виде.

Плюсы:

  • Меньший размер данных (на 20-50% меньше по сравнению с JSON).

  • Высокая скорость парсинга (не нужно разбирать текст).

  • Поддерживается во многих языках, включая JavaScript/Node.js (msgpack5).

Минусы:

  • Меньшая человеко-читаемость (в браузере не так удобно дебажить).

  • Нужно сторонними средствами смотреть, что внутри (нужен декодер).

Пример (Node.js с msgpack5):

const msgpack = require('msgpack5')();

const data = {
  user: 'Alice',
  age: 30,
  scores: [10, 20, 30]
};

const packed = msgpack.encode(data);
console.log('Packed buffer:', packed);

const unpacked = msgpack.decode(packed);
console.log('Unpacked:', unpacked);

В реальном проекте MessagePack может дать выигрыш в скорости и объёме передаваемых данных, особенно когда речь идёт о высоконагруженных сервисах.

Protocol Buffers (Protobuf) - двоичный формат от Google, в котором обязательно описывать структуру данных в .proto-файле (схема). На базе этой схемы генерируется код (классы) для различных языков.

Плюсы:

  • Высокая производительность (парсинг, размер данных).

  • Строгая типизация и версионирование.

  • Идеально подходит для микросервисов на gRPC.

Минусы:

  • Нужно поддерживать .proto-схему и генерировать код.

  • Меньшая гибкость в сравнении с JSON (сложно передавать «произвольные» структуры).

Упрощенный пример. Схема (user.proto):

syntax = "proto3";

message User {
  string name = 1;
  int32 age = 2;
  repeated int32 scores = 3;
}

Устанавливаем protoc и плагин для Node.js, генерируем JS-код. В итоге получаем файлы вида user_pb.js.

const messages = require('./user_pb'); // сгенерированный код

const user = new messages.User();
user.setName('Alice');
user.setAge(30);
user.setScoresList([10, 20, 30]);

const bytes = user.serializeBinary();
console.log('Binary length:', bytes.length);

const user2 = messages.User.deserializeBinary(bytes);
console.log(user2.getName(), user2.getAge(), user2.getScoresList());

Применять Protobuf в веб-разработке имеет смысл, если вы строите масштабируемую систему микросервисов на gRPC или действительно заботитесь о каждом килобайте и миллисекунде. Однако для большинства веб-API, где удобнее быстро смотреть структуру в сыром виде, JSON остаётся основным форматом.

Заключение и рекомендации для веб-разработчиков

  • Проверяйте точность чисел - не полагайтесь на то, что большие order_id или balance всегда поместятся в Number.

  • Храните и передавайте даты в едином формате (ISO 8601 с UTC) - чтобы избежать путаницы с часовыми поясами.

  • Стримьте большие JSON - Node.js позволяет легко обрабатывать файлы и потоки, не загружая всё в память.

  • Используйте компрессию (gzip, brotli) - это ускорит передачу JSON через HTTP.

  • Рассмотрите альтернативы (MessagePack, Protobuf), если у вас высокие требования к производительности и объёму трафика и вы готовы поддерживать двоичный формат (особенно когда надо экономить ресурсы).

Вместо послесловия

В большинстве веб-проектов JSON остаётся удобным и достаточным решением. Однако стоит помнить о его подводных камнях: потеря точности чисел, отсутствие встроенной даты, большие объёмы данных, которые могут убить производительность и съесть всю память. Если вы развиваете сложный сервис с высоконагруженными компонентами, возможно, стоит присмотреться к более эффективным форматам, вроде MessagePack или ProtoBuf. Но для подавляющего большинства случаев JSON в связке с Node.js (и правильным стримингом/компрессией) будет надёжным выбором.

Удачной веб-разработки!

Комментарии (6)


  1. somech
    12.01.2025 07:06

    При чем тут вообще JSON во всех приведенных проблемах? Давайте разберем заключение:

    1-2 -- JSON вообще не при чем. В XML будут те-же проблемы. Да даже со стрингой.

    3 -- проблемы большого объема данных, актуально для любого формата.

    4 -- максимально капитанское объяснение, которое, опять таки, не проблема конкретно JSONа как формата данных. Не сжимает данные нынче только ленивый

    5 -- альтернативы есть. Ну хорошо.

    Подведя собственное "заключение" :а был ли при чем тут JSON?


  1. andry36 Автор
    12.01.2025 07:06

    спасибо за фидбек!

    соглашусь, что подобные сложности (большие числа, даты, объёмные данные) могут возникать в любом текстовом формате, не только JSON. Однако JSON сегодня является стандартом для большинства веб-приложений на JavaScript/Node.js, поэтому именно в нём эти грабли встречаются чаще всего. Статья призвана показать, как обходить их в контексте JSON и когда стоит рассмотреть альтернативные форматы.

    Правильно ли я понимаю, что вас смущает именно то, что в статье рассмотрены проблемы в контексте JSON (хотя они встречаются и в других форматах), и сам заголовок подаёт их как уникальные для JSON?


    1. dyadyaSerezha
      12.01.2025 07:06

      Комментатор прав в том, что, исходя из названия статьи, ожидаешь увидеть нечто другое. В статье же обсуждаются не скрытые ошибки/проблемы JSON (от слова "подвести"), а совершенно публичные и понятные свойства или ограничения формата. Поэтому название и содержание статьи несколько не коррелируют.

      Это как написать в статье "jpeg. может вас подвести", что при стриминге видео не самая удачная идея пересылать отдельные кадры как jpeg-картинки, а лучше использовать специальный видео-формат с гораздо большим сжатием и другими фишками.


  1. mds-oof
    12.01.2025 07:06

    Я, конечно, понимаю, что хочется вставить красивое изображение в пост. Но изображения от ИИ все же надо обрабатывать. Иначе создается ощущение, что писали наспех и лишь бы опубликовать.

    Давайте вместе посмотрим на изображение. В центре "файл" JSON. А что внутри него? Скобочки, музыкальные ноты, дефисы (не говоря уже про надписи "JSON Pastlets" вокруг, чтобы это не значило). Чтобы исправить это потребуется примерно минут 5-10. Затереть эту глупость и вставить похожим шрифтом хотя бы что-то отдаленно напоминающее JSON, или вообще оставив пустой файлик. И это смотрелось бы гармоничнее. И статья выглядела бы более зрело.


    1. lightman
      12.01.2025 07:06

      +1

      Неужели авторы постов не видят, как отвратительно выглядят эти изображения, что изображения с искажениями вызывают какое-то глубокое отторжение из разряда эффекта зловещей долины?

      Хотел тут послушать подборку рождественских песен на нельзятубе - так весь ролик был сгенерирован ИИ. Какое-то время боролся с собой, старался не смотреть, только слушать (песни то классические, прекрасные), но не смог, переключил.

      Решил отдохнуть от компьютера, вышел на улицу прогуляться - так даже на уличном баннере вставили на фон. Просто какая-то эпидемия. Или т.н. "контент мейкеры" считают что "да пофиг, они не заметят разницы". Ещё как замечаем! Омерзительная искажённая бездушность ИИ изображений и видео сразу бросается глаза - неужели у вас атрофировано чувство эстетики и вы сами этого не видите?

      Администрация хабра, пожалуйста, добавьте в список причин при минусе поста пункт "использование ИИ изображений". Сейчас приходится выбирать "пост небрежно оформлен", но это не совсем то и не даёт автору полезной обратной связи.


  1. monochromer
    12.01.2025 07:06

    Для экономии места и ускорения передачи больших JSON-ответов по HTTP в продакшене практически всегда включают gzip или brotli-сжатие

    app.use(compression()); // Включаем сжатие

    Только сжатие лучше перенести на уровень proxy-сервера, например,  Nginx или Caddy.