Управление JSON для конфигурации, фикстур и локальных данных
Главное: Конфигурация должна быть слоистой и валидируемой; фикстуры должны быть детерминированными, читаемыми и diff-пригодными. Если эти два типа файлов управляются плохо, стоимость диагностики остаётся высокой долго.

Главное: постройте конфигурацию как значения по умолчанию, окружение и локальные переопределения с валидацией при запуске, прежде чем беспокоиться о формате файла; сделайте фикстуры детерминированными во времени, ID, порядке ключей и отступах, чтобы они оставались diff-пригодными, и только затем автоматизируйте маскирование.
Эти два типа JSON не участвуют в бизнес-логике, но они определяют «сколько времени нужно новому коллеге, чтобы запуститься» и «сколько времени нужно, чтобы локализовать производственную проблему».
1. Конфигурация: трёхслойная структура
| Слой | Источник | Поддерживает | Коммитится |
|---|---|---|---|
| Значения по умолчанию | Константы в коде / default.json |
Разработчики | Да |
| Конфигурация окружения | config/production.json, переменные окружения |
Операции | Да (без секретов) |
| Локальные переопределения | .env.local, config/local.json |
Отдельные люди | Нет (в gitignore) |
Порядок слияния: значения по умолчанию → окружение → локальные, при этом каждый последующий слой переопределяет предыдущий. Сливайте только настолько глубоко, насколько нужно бизнесу; не слияйте глубоко слепо — глубокое слияние делает проблемы вроде «массивы неожиданно конкатенировались» крайне трудно заметными.
/**
* Merges configuration layers by priority; later layers override earlier ones, and arrays are replaced wholesale rather than concatenated.
* @param layers Configuration layers from lowest to highest priority
* @returns The merged configuration object
*/
function mergeConfigLayers(layers: Array<Record<string, unknown>>): Record<string, unknown> {
return layers.reduce<Record<string, unknown>>((mergedConfig, currentLayer) => {
for (const [configKey, configValue] of Object.entries(currentLayer)) {
const isPlainObject =
typeof configValue === 'object' && configValue !== null && !Array.isArray(configValue);
const existingValue = mergedConfig[configKey];
if (isPlainObject && typeof existingValue === 'object' && existingValue !== null) {
mergedConfig[configKey] = mergeConfigLayers([
existingValue as Record<string, unknown>,
configValue as Record<string, unknown>,
]);
continue;
}
// Arrays and scalars are always replaced wholesale to avoid accidental element concatenation
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
2. Валидируйте при запуске, не ждите времени выполнения
Стоимость ошибки конфигурации растёт экспоненциально с тем, насколько поздно она обнаружена:
| Когда обнаружена | Стоимость |
|---|---|
| При запуске | Одна строка лога, перезапуск и дальше |
| На первом запросе | Некоторые функции падают, час отладки |
| Когда срабатывает плановая задача | Оповещения посреди ночи |
| В день расчётов | Инцидент сверки |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* Validates the configuration and throws immediately on failure, implementing fail fast.
* @param rawConfig The raw merged configuration
* @returns The validated configuration
*/
export function assertConfig(rawConfig: unknown): Record<string, unknown> {
const validate = ajv.compile(configSchema);
if (!validate(rawConfig)) {
const messages = (validate.errors ?? []).map(
(errorItem) => `${errorItem.instancePath || '/'} ${errorItem.message ?? ''}`,
);
throw new Error(`配置校验失败:\n${messages.join('\n')}`);
}
return rawConfig as Record<string, unknown>;
}
Поддерживающие принципы:
- Немедленно выходите, когда обязательный элемент отсутствует; не подставляйте «правдоподобный» дефолт (например, адрес базы данных по умолчанию
localhost); - Немедленно выходите при ошибках типа; строка
"false"— не булевоfalse, поэтому переменные окружения нужно преобразовывать явно после чтения; - Секреты никогда не попадают в репозиторий; читайте их только из переменных окружения или сервиса управления секретами.
3. Какой формат использовать для конфигурационных файлов
| Файл | Формат | Причина |
|---|---|---|
package.json |
Стандартный JSON | Менеджер пакетов принимает только его |
tsconfig.json |
JSONC | Комментарии разрешены, официально поддерживается |
.vscode/settings.json |
JSONC | То же самое |
| Бизнес-конфигурация | YAML или JSONC | Нужны комментарии, объясняющие, что значит каждый пункт |
| Список переменных окружения | .env |
Плоские пары ключ-значение, легко вставляются |
Практический тест: будет ли этот файл читать нечеловеческий инструмент? Если да → стандартный JSON; если его читает только ваш собственный код → диалект подойдёт.
4. Тестовые фикстуры: каталоги и именование
tests/fixtures/
├─ valid/
│ ├─ order-minimal.json
│ ├─ order-full.json
│ └─ order-with-null-fields.json
├─ invalid/
│ ├─ order-missing-id.json
│ ├─ order-price-as-number.json
│ └─ order-nested-broken.json
└─ edge/
├─ big-integer.json
├─ emoji-and-surrogate.json
└─ deep-nesting-64.json
Хорошее соглашение об именовании — объект-сценарий: order-missing-field гораздо полезнее, чем test1.json.
Не-ASCII имена файлов иногда вызывают проблемы с кодировкой в кроссплатформенных инструментальных цепочках, поэтому держите имена файлов на английском, пока содержимое может оставаться на китайском.
4.1 Три вида случаев, три вида ожиданий
| Каталог | Ожидание | Назначение |
|---|---|---|
valid/ |
Валидация проходит | Основной поток, генерация типов |
invalid/ |
Валидация падает, и ошибка указывает на ожидаемое поле | Обработка ошибок, текст сообщений |
edge/ |
Поведение явное (проходить или падать допустимо, но должно быть проверено) | Защита от регрессий |
JSONTestSuite использует префиксы y_ / n_ / i_ для тех же трёх видов (должно принимать / должно отвергать / определяется реализацией),
и вы можете напрямую заимствовать это соглашение об именовании.
5. Детерминизм: сделайте фикстуры diff-пригодными
Нестабильные фикстуры делают ревью кода бесполезным:
| Источник шума | Как обрабатывать |
|---|---|
| Текущее время | Закрепите его константой, например 2026-01-01T00:00:00Z |
| Случайные ID / UUID | Используйте возрастающую последовательность или фиксированные значения |
| Порядок ключей | Сортируйте по алфавиту во время генерации |
| Форматирование чисел с плавающей точкой | Единообразно держите два знака после запятой или преобразуйте в строки |
| Часовой пояс | Используйте UTC везде |
| Отступы | Фиксированные 2 пробела, проверяемые Prettier в CI |
/**
* Recursively sorts keys to produce stable, diffable JSON text.
* @param value The value to serialize
* @param indentWidth Indentation width
* @returns A JSON string with a stable order
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* Recursively sorts object keys.
* @param input Any JSON value
* @returns A new value with sorted keys
*/
const sortKeysDeep = (input: unknown): unknown => {
if (Array.isArray(input)) {
return input.map((item) => sortKeysDeep(item));
}
if (typeof input === 'object' && input !== null) {
return Object.fromEntries(
Object.keys(input)
.sort()
.map((entryKey) => [entryKey, sortKeysDeep((input as Record<string, unknown>)[entryKey])]),
);
}
return input;
};
return JSON.stringify(sortKeysDeep(value), null, indentWidth);
}
6. Маскирование и соответствие требованиям
Реальные данные нельзя использовать напрямую как фикстуры:
- Имена → заполнители вроде «John Doe» и «Jane Roe»;
- Телефоны → явно вымышленные номера вроде
13800138000; - Номера удостоверений → фальшивые номера с правильным форматом, но недействительной контрольной цифрой;
- Адреса → сохраните структуру провинции/города, отбросьте номер дома;
- Суммы → сохраните порядок величины, но никогда не храните реальную информацию о счёте.
Лучше всего, чтобы у команды был scripts/mask-fixture.mjs, который прогоняется по экспортированному производственному образцу и генерирует замаскированную версию,
избегая «изменить три места вручную, забыть остальное».
7. Миграция и версии
Фикстуры тоже устаревают. Добавьте поле schemaVersion в фикстуру или поместите файл VERSION в каталог:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Когда контракт API обновляется:
- Добавьте новый каталог фикстур (
v3/); не изменяйте старые фикстуры на месте; - Держите старые фикстуры для регрессии, пока старая версия API не выведена из эксплуатации;
- Запускайте обе версии в CI, чтобы подтвердить обратную совместимость.
8. Контрольный список
- [ ] Трёхслойная структура конфигурации ясна, и локальные переопределения не коммитятся;
- [ ] Валидация происходит при запуске, и сбой немедленно завершает работу;
- [ ] Секреты приходят только из переменных окружения; конфигурационные файлы содержат только заполнители;
- [ ] Фикстуры разделены на каталоги
valid/invalid/edge, и имена выражают сценарий; - [ ] Фикстуры детерминированы (фиксированное время, ID и порядок) и diff-пригодны;
- [ ] Реальные данные обработаны скриптом маскирования;
- [ ] Обновления контракта добавляют новый каталог версии, а не редактируют на месте.
Вопросы и ответы
Как упорядочивать и сливать слои конфигурации?
Используйте три слоя — значения по умолчанию, конфигурация окружения и локальные переопределения — слияние значений по умолчанию → окружение → локальные, при этом каждый последующий слой переопределяет предыдущий. Сливайте только настолько глубоко, насколько нужно бизнесу, и заменяйте массивы и скаляры целиком, чтобы глубокое слияние не могло неожиданно конкатенировать массивы. Добавьте локальный файл переопределений в gitignore, а не коммитьте личные настройки.
Почему валидировать конфигурацию при запуске?
Стоимость ошибки конфигурации растёт экспоненциально с тем, насколько поздно она обнаружена: одна строка лога при запуске против инцидента сверки в день расчётов. Валидируйте по JSON Schema при запуске и немедленно завершайте работу при отсутствующих обязательных пунктах или ошибках типов, вместо подстановки «правдоподобного» дефолта.
Стоит ли использовать производственные данные напрямую как тестовые фикстуры?
Нет. Имена, телефоны, номера удостоверений, адреса и суммы — всё нуждается в заполнителях или явно вымышленных значениях, и у команды должен быть scripts/mask-fixture.mjs, который прогоняется по производственному экспорту и генерирует замаскированную версию, избегая ручных правок, пропускающих что-то.