Почему текстовые форматы не идеальны в разработке: пример на JSON

от автора

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

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

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

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

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

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

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

Неочевидные проблемы при сериализации и десериализации 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 (и правильным стримингом/компрессией) будет надёжным выбором.

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


ссылка на оригинал статьи https://habr.com/ru/articles/871616/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *