Керування 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 оновлюється:
- Додайте новий каталог тестових даних (
v3/); не змінюйте старі на місці; - Тримайте старі тестові дані для регресії, доки стару версію API не виведено;
- Запускайте обидві версії в CI, щоб підтвердити зворотну сумісність.
8. Чек-лист
- [ ] Тришарова структура конфігурації ясна, а локальні перевизначення не комітяться;
- [ ] Перевірка відбувається під час запуску, а збій негайно завершує роботу;
- [ ] Секрети надходять лише зі змінних середовища; конфігураційні файли містять лише заповнювачі;
- [ ] Тестові дані розділено на каталоги
valid/invalid/edge, а назви виражають сценарій; - [ ] Тестові дані детерміновані (фіксований час, ID і порядок) і оглядопридатні;
- [ ] Реальні дані оброблено скриптом маскування;
- [ ] Оновлення контракту додає новий каталог версії, а не редагує на місці.
Часті питання
Як упорядковувати й зливати шари конфігурації?
Використовуйте три шари — значення за замовчуванням, конфігурація середовища й локальні перевизначення — зливаючи замовчування → середовище → локально, кожен наступний шар перекриває попередній. Зливайтеся лише настільки глибоко, наскільки потрібно бізнесу, і замінюйте масиви й скаляри цілком, щоб глибоке злиття не склеювало масиви несподівано. Додайте файл локальних перевизначень у gitignore, а не комітьте особисті налаштування.
Чому перевіряти конфігурацію під час запуску?
Ціна помилки конфігурації зростає експоненційно з тим, наскільки пізно її виявлено: один рядок логу під час запуску проти інциденту зі звіркою в день розрахунків. Перевіряйте за JSON Schema під час запуску й негайно виходьте на відсутніх обовʼязкових елементах чи помилках типів, замість підставляти «правдоподібне» значення за замовчуванням.
Чи використовувати виробничі дані безпосередньо як тестові?
Ні. Імена, номери телефонів, номери ID-карток, адреси й суми всі потребують заповнювачів або очевидно фіктивних значень, і команда має мати scripts/mask-fixture.mjs, що проходить над виробничим експортом, генеруючи замасковану версію, уникаючи ручних правок, які щось пропускають.