Почему текстовые форматы не идеальны в разработке: пример на JSON
- понедельник, 13 января 2025 г. в 00:00:04
Ни для кого не секрет, что JSON широко используется в веб-разработке: обмен данными между клиентом (браузером) и сервером, хранение в NoSQL-базах, конфигурационные файлы, API-ответы и многое другое. Он стал практически родным форматом данных для JavaScript и Node.js. Однако при работе с JSON стоит учитывать ряд ограничений и подводных камней, которые в больших проектах могут вылиться в серьёзные проблемы с производительностью, точностью и безопасностью.
В этой статье мы разберём:
Неочевидные проблемы при сериализации/десериализации JSON - с фокусом на веб-разработку.
Обработку больших JSON-файлов - нюансы и инструменты в Node.js (и не только).
Популярные альтернативы JSON: MessagePack и Protocol Buffers - когда и как их стоит применять в веб-приложениях.
Статья рассчитана на веб-разработчиков, которые работают с JSON каждый день и хотят глубже разобраться в его особенностях, а также расширить свой стек инструментов.
Важно отметить, что описанные в статье проблемы не являются исключительной особенностью JSON - они присущи любым текстовым форматам (XML, YAML и др.). Мы сосредоточились на JSON как на самом популярном варианте в веб-разработке и Node.js-экосистеме, но все приведённые грабли встречаются и в других форматах.
В 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 не содержит встроенного типа даты/времени. В веб-среде чаще всего встречаются три подхода:
ISO 8601: 2025-01-04T12:34:56Z
Unix Timestamp (в секундах или миллисекундах): 1672822561000
Пользовательские форматы: 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.
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-ответы на десятки мегабайт, а потом удивляются долгому парсингу и высокому расходу памяти.
На стороне клиента: JSON.parse
крупного ответа (особенно в мобильном браузере) может подвесить UI на несколько секунд.
На стороне сервера (Node.js): JSON.stringify
очень большого объекта тоже затратен.
Если ваше веб-приложение возвращает огромные JSONы:
Подумайте, действительно ли нужно отдавать всё сразу. Возможно, лучше сделать пагинацию, lazy-load или частичные данные.
Используйте стриминг там, где это уместно (Node.js stream
+ JSON chunking).
Сжимайте ответ (например, gzip
или brotli
в Express через compression).
В 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 (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 (объекты, массивы, строки, числа), но в более компактном виде.
Плюсы:
Меньший размер данных (на 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 (и правильным стримингом/компрессией) будет надёжным выбором.
Удачной веб-разработки!