JSON Toolkit

Управление JSON для конфигурации, фикстур и локальных данных

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

Начинающий~10 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Обложка управления JSON для конфигурации, фикстур и локальных данных

Главное: постройте конфигурацию как значения по умолчанию, окружение и локальные переопределения с валидацией при запуске, прежде чем беспокоиться о формате файла; сделайте фикстуры детерминированными во времени, 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 обновляется:

  1. Добавьте новый каталог фикстур (v3/); не изменяйте старые фикстуры на месте;
  2. Держите старые фикстуры для регрессии, пока старая версия API не выведена из эксплуатации;
  3. Запускайте обе версии в CI, чтобы подтвердить обратную совместимость.

8. Контрольный список

  • [ ] Трёхслойная структура конфигурации ясна, и локальные переопределения не коммитятся;
  • [ ] Валидация происходит при запуске, и сбой немедленно завершает работу;
  • [ ] Секреты приходят только из переменных окружения; конфигурационные файлы содержат только заполнители;
  • [ ] Фикстуры разделены на каталоги valid/invalid/edge, и имена выражают сценарий;
  • [ ] Фикстуры детерминированы (фиксированное время, ID и порядок) и diff-пригодны;
  • [ ] Реальные данные обработаны скриптом маскирования;
  • [ ] Обновления контракта добавляют новый каталог версии, а не редактируют на месте.

Вопросы и ответы

Как упорядочивать и сливать слои конфигурации?

Используйте три слоя — значения по умолчанию, конфигурация окружения и локальные переопределения — слияние значений по умолчанию → окружение → локальные, при этом каждый последующий слой переопределяет предыдущий. Сливайте только настолько глубоко, насколько нужно бизнесу, и заменяйте массивы и скаляры целиком, чтобы глубокое слияние не могло неожиданно конкатенировать массивы. Добавьте локальный файл переопределений в gitignore, а не коммитьте личные настройки.

Почему валидировать конфигурацию при запуске?

Стоимость ошибки конфигурации растёт экспоненциально с тем, насколько поздно она обнаружена: одна строка лога при запуске против инцидента сверки в день расчётов. Валидируйте по JSON Schema при запуске и немедленно завершайте работу при отсутствующих обязательных пунктах или ошибках типов, вместо подстановки «правдоподобного» дефолта.

Стоит ли использовать производственные данные напрямую как тестовые фикстуры?

Нет. Имена, телефоны, номера удостоверений, адреса и суммы — всё нуждается в заполнителях или явно вымышленных значениях, и у команды должен быть scripts/mask-fixture.mjs, который прогоняется по производственному экспорту и генерирует замаскированную версию, избегая ручных правок, пропускающих что-то.