Чтение и запись JSON в Node.js
Зачем работать с JSON в Node.js
JSON — это текстовый формат для обмена структурированными данными. Он лёгкий для чтения человеком и прост для парсинга программами. В Node.js чтение и запись JSON используются для конфигураций, локальных «баз данных» для небольших проектов, экспортов/импортов и промежуточного кэширования.
Кратко: JSON хранит объекты и массивы в виде строки. JSON.parse преобразует строку в объект, JSON.stringify — объект в строку.
Модуль файловой системы Node.js
Node.js содержит встроенный модуль fs для работы с файлами. Он предоставляет синхронные и асинхронные методы. Асинхронные методы не блокируют цикл событий и рекомендуются для серверных приложений. Синхронные методы удобны в простых скриптах и для однопоточных задач при инициализации.
Современный способ — импортировать промис-версию API:
const fs = require("node:fs/promises"); Если у вас версия Node.js ниже v18, используйте:
const fs = require("fs/promises"); Чтобы получить и синхронный, и асинхронный функционал, импортируйте без суффикса /promises.
Важно: всегда выбирать асинхронные операции для кода, обслуживающего запросы пользователей, чтобы не блокировать сервер.
Чтение JSON файлов
Метод readFile принимает путь к файлу и опции. Опции могут быть строкой кодировки или объектом с полями encoding и flag.
Часто используемые опции:
- encoding: строка, по умолчанию “utf8”. Обычно для JSON используйте “utf-8”.
- flag: строка, например “r” для чтения или “w” для записи.
Пример асинхронного чтения и парсинга:
fs.readFile("./users.json", { encoding: "utf-8", flag: "r" })
.then((data) => {
const users = JSON.parse(data);
console.log(users);
})
.catch((error) => {
console.error('Error reading the JSON file:', error);
}); Советы при чтении:
- Всегда оборачивайте JSON.parse в try/catch или обрабатывайте ошибку промиса. Неверный JSON вызовет исключение.
- Для малых конфигурационных файлов можно использовать require(‘./config.json’), но помните про кэширование. require кеширует содержимое модуля — изменения на диске не загрузятся автоматически.
- Для больших файлов рассмотрите стриминг или разбивку на NDJSON.
Запись JSON файлов
Метод writeFile асинхронно записывает данные. Аргументы: путь, данные (строка или буфер), опции.
Пример базовой записи:
const fakeUsers = [
{
id: 1,
name: "John Doe",
username: "johndoe123",
address: {
street: "123 Main St",
city: "Anytown",
},
},
{
id: 2,
name: "Jane Smith",
username: "janesmith456",
address: {
street: "456 Elm St",
city: "Another City",
},
}
];
fs.writeFile("./users.json", JSON.stringify(fakeUsers), {
encoding: "utf-8",
flag: "w",
}).catch((error) => {
console.error('Error writing the JSON file:', error);
}); Рекомендации для записи:
- Преобразуйте объекты в строку через JSON.stringify. Можно передать второй аргумент replacer и третий параметр для отступов, например JSON.stringify(obj, null, 2) для читаемого вывода.
- Избегайте прямого перезаписывания критичных файлов без атомарной операции.
Атомарная запись и защита от частичного файла
Запись файла напрямую может привести к частично записанному файлу при сбое. Универсальный приём, обеспечивающий атомичность на большинстве платформ:
- Записать данные во временный файл в той же директории, например users.json.tmp.
- Переименовать временный файл в users.json (fs.rename). Операция переименования обычно атомарна на одном разделе файловой системы.
Пример функции атомарной записи:
const path = require('path');
async function writeAtomically(filePath, data) {
const dir = path.dirname(filePath);
const tempPath = path.join(dir, `${path.basename(filePath)}.tmp`);
await fs.writeFile(tempPath, data, { encoding: 'utf-8', flag: 'w' });
await fs.rename(tempPath, filePath);
}
// Использование
writeAtomically('./users.json', JSON.stringify(fakeUsers, null, 2)).catch(console.error);Примечание: атомарное переименование надежно на POSIX и большинстве современных файловых систем. На некоторых сетевых томах или при смешанных файловых системах гарантии могут отличаться.
Обновление JSON файлов
fs не имеет специального метода обновления. Подход прост:
- Прочитать файл.
- Распарсить в память.
- Изменить структуру в памяти.
- Записать обратно (лучше атомарно).
Пример функции обновления:
const updateFile = async (filePath, data) => {
try {
const fileContents = await fs.readFile(filePath, {
encoding: "utf-8",
flag: "r",
});
const fileData = JSON.parse(fileContents);
const updatedFileData = [...fileData, ...data];
await fs.writeFile(filePath, JSON.stringify(updatedFileData), {
encoding: "utf-8",
flag: "w",
});
return "File updated successfully";
} catch (error) {
console.error('Error updating the JSON file:', error);
}
}; Вызов:
updateFile("./users.json", [ /* новые записи */ ]).then((message) => {
console.log(message);
});Проблемы конкурентного доступа:
- Если несколько процессов параллельно читают и записывают один файл, возможна потеря данных. Решения: очередь обновлений, внешняя блокировка (lockfile), использование СУБД или использование atomic rename вместе с уникальными временными файлами и контрольной логикой с повтором при конфликте.
Работа с большими JSON и альтернатива NDJSON
Если файл слишком большой, чтение целиком может привести к нехватке памяти. Варианты:
- Стриминг JSON не тривиален для массивов, но возможен с парсерами потоков (например, JSONStream или stream-json).
- Использовать NDJSON (newline-delimited JSON): одна JSON запись на строку. Тогда можно читать и обрабатывать файл построчно.
- Рассмотреть хранение в базе данных (SQLite, LevelDB, PostgreSQL) для больших или частых обновлений.
Валидация и схемы
Перед использованием данных из JSON проверяйте структуру и типы. Подходы:
- Лёгкая ручная валидация: проверка обязательных полей и типов.
- Схемы JSON Schema с валидаторами (ajv и другие).
- Преобразование и однотипные DTO на входе.
Пример с ajv для проверки структуры пользователей — сократить ошибки на ранней стадии.
Безопасность и защита данных
Основные меры:
- Валидировать и санитизировать входные данные перед сохранением.
- Не исполнять код из JSON. Никогда не eval данных из файла.
- Ограничить доступ к файлам по правам: устанавливайте минимально необходимые права на директории и файлы.
- Использовать path.normalize и allowlist директорий, чтобы предотвратить directory traversal при работе с путями от пользователя.
- Шифровать чувствительные поля или весь файл, если он хранит персональные данные.
- Логи и ошибки не должны раскрывать содержимое файлов с персональными данными.
Примеры проверок пути
const path = require('path');
function safeJoin(base, target) {
const resolved = path.resolve(base, target);
if (!resolved.startsWith(path.resolve(base))) {
throw new Error('Недопустимый путь');
}
return resolved;
}Конкретные сценарии и подходы
Когда использовать что:
- Конфигурационные файлы на старте приложения: можно читать синхронно при инициализации.
- Логирование событий и большие записи: лучше использовать ротацию логов и структурированные логи, а не постоянно перезаписывать JSON.
- Частые конкурентные обновления: используйте базу данных или механизм очередей.
- Встроенное хранилище для тестов и маленьких утилит: локальные JSON-файлы подходят.
Критерии приёмки
- Файлы JSON читаются без исключений при корректном формате.
- При повреждении JSON возвращается понятная ошибка и сохраняется бэкап, если это возможно.
- Запись выполняется атомарно: либо старое содержимое, либо новое, но не частично записанный файл.
- Права доступа установлены по принципу минимально необходимого доступа.
- Автоматические тесты покрывают случаи некорректного JSON, отсутствие файла и параллельные обновления.
План действий при инциденте с файлом
- Остановить запись в проблемный файл (при возможности).
- Сделать копию текущего файла для расследования.
- Восстановить последнюю рабочую версию из бэкапа или версионного контроля.
- Прописать проверку целостности и валидности при следующей записи.
- Если данные персональные, уведомить ответственных по защите данных согласно регламенту.
Тесты и приёмочные сценарии
- Позитивный тест: корректный JSON читается и преобразуется в нужную структуру.
- Негативный тест: повреждённый JSON вызывает читаемую ошибку, приложение не падает непредсказуемо.
- Тест параллельных записей: два клиента пытаются добавить запись одновременно — итоговые данные не теряются при корректной механике обновления.
- Тест больших файлов: чтение и обработка не приводят к OOM при использовании стриминга.
Чеклист для разработки и деплоя
- Используются асинхронные методы fs в рабочем коде.
- Добавлена проверка JSON.parse и обработка ошибок.
- Запись реализована атомарно или используется СУБД.
- Настроена ротация и бэкап критичных файлов.
- Ограничены права доступа на директории и файлы.
- Проведены тесты конкурентного доступа.
Локальные альтернативы и миграция
Если проект растёт, рассмотрите миграцию с файлов на подходящую СУБД:
- Для простоты и встроенности — SQLite.
- Для множества одновременных пользователей — PostgreSQL или MongoDB.
- Для простого ключ-значение — LevelDB или Redis.
Миграция обычно включает экспорт JSON → импорт в базу с валидацией схемы.
1‑строчный глоссарий
- JSON — формат обмена данными в текстовом виде.
- fs — модуль Node.js для работы с файловой системой.
- Атомарная запись — техника, гарантирующая «всё или ничего» при сохранении файла.
- NDJSON — формат, где каждая строка — отдельный JSON-объект.
Заключение
Чтение и запись JSON в Node.js просты, но требуют дисциплины: используйте асинхронные вызовы, валидируйте данные, применяйте атомарную запись и учитывайте особенности больших файлов и конкурентного доступа. Для серьёзных требований по консистентности и масштабу лучше выбрать специализированное хранилище.
Важно: адаптируйте стратегию под размер данных, количество клиентов и требования к безопасности.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone