JSON Toolkit

Керування JSON для конфігурації, тестових даних і локальних даних

Головне: Конфігурація має бути шаруватою й перевіреною; тестові дані — детермінованими, читабельними й оглядопридатними. Якщо цими двома видами файлів керувати погано, ціна діагностики довго залишається високою.

Початковий~10 хв читанняОпубліковано 2026-10-30Оновлено 2026-10-30
Обкладинка статті про керування JSON для конфігурації, тестових даних і локальних даних

Коротко: будуйте конфігурацію як значення за замовчуванням, середовище й локальні перевизначення з перевіркою під час запуску, перш ніж дбати про формат файлу; робіть тестові дані детермінованими в часі, ID, порядку ключів і відступах, щоб вони залишалися оглядопридатними, і лише потім автоматизуйте маскування.

Ці два види 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

Добра конвенція найменування — object-scenario: order-missing-field набагато корисніше за test1.json. Файли з не-ASCII назвами іноді спричиняють проблеми кодування в міжплатформних інструментаріях, тож тримайте назви файлів англійською, а вміст може залишатися китайською.

4.1 Три види випадків, три види очікувань

Каталог Очікування Призначення
valid/ Перевірка проходить Основний потік, генерація типів
invalid/ Перевірка зазнає збою, і помилка вказує на очікуване поле Обробка помилок, текст повідомлень
edge/ Поведінка явна (проходить чи збоїть — байдуже, але має бути перевірено) Захист від регресій

JSONTestSuite використовує префікси y_ / n_ / i_ для тих самих трьох видів (має прийняти / має відхилити / визначено реалізацією), і ви можете позичити цю конвенцію найменування безпосередньо.

5. Детермінізм: робіть тестові дані оглядопридатними

Нестабільні тестові дані роблять рев'ю коду марним:

Джерело шуму Як обробити
Поточний час Фіксуйте до константи, наприклад 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;
  • Номери ID-карток → фальшиві номери з правильним форматом, але недійсною контрольною цифрою;
  • Адреси → збережіть структуру провінції/міста, відкиньте номер будинку;
  • Суми → збережіть порядок величини, але ніколи не зберігайте реальну інформацію про рахунок.

Найкраще, щоб команда мала 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 і порядок) і оглядопридатні;
  • [ ] Реальні дані оброблено скриптом маскування;
  • [ ] Оновлення контракту додає новий каталог версії, а не редагує на місці.

Часті питання

Як упорядковувати й зливати шари конфігурації?

Використовуйте три шари — значення за замовчуванням, конфігурація середовища й локальні перевизначення — зливаючи замовчування → середовище → локально, кожен наступний шар перекриває попередній. Зливайтеся лише настільки глибоко, наскільки потрібно бізнесу, і замінюйте масиви й скаляри цілком, щоб глибоке злиття не склеювало масиви несподівано. Додайте файл локальних перевизначень у gitignore, а не комітьте особисті налаштування.

Чому перевіряти конфігурацію під час запуску?

Ціна помилки конфігурації зростає експоненційно з тим, наскільки пізно її виявлено: один рядок логу під час запуску проти інциденту зі звіркою в день розрахунків. Перевіряйте за JSON Schema під час запуску й негайно виходьте на відсутніх обовʼязкових елементах чи помилках типів, замість підставляти «правдоподібне» значення за замовчуванням.

Чи використовувати виробничі дані безпосередньо як тестові?

Ні. Імена, номери телефонів, номери ID-карток, адреси й суми всі потребують заповнювачів або очевидно фіктивних значень, і команда має мати scripts/mask-fixture.mjs, що проходить над виробничим експортом, генеруючи замасковану версію, уникаючи ручних правок, які щось пропускають.