Zarządzanie JSON dla konfiguracji, danych testowych i danych lokalnych
Podsumowanie: Konfiguracja musi być warstwowa i walidowana; dane testowe muszą być deterministyczne, czytelne i nadawać się do diffu. Jeśli te dwa rodzaje plików są zarządzane słabo, koszt debugowania pozostaje wysoki przez długi czas.

Najważniejsze: buduj konfigurację jako wartości domyślne, środowisko i nadpisania lokalne, z walidacją przy starcie, zanim zaczniesz martwić się o format pliku; uczyń dane testowe deterministycznymi w czasie, identyfikatorach, kolejności kluczy i wcięciach, aby pozostały diffowalne, a dopiero potem automatyzuj maskowanie.
Te dwa rodzaje JSON nie biorą udziału w logice biznesowej, a mimo to decydują o tym, „ile czasu zajmie nowemu współpracownikowi uruchomienie” i „ile zajmie zlokalizowanie produkcyjnego problemu”.
1. Konfiguracja: struktura trójwarstwowa
| Warstwa | Źródło | Utrzymywana przez | Commitowana |
|---|---|---|---|
| Wartości domyślne | Stałe w kodzie / default.json |
Deweloperów | Tak |
| Konfiguracja środowiska | config/production.json, zmienne środowiskowe |
Operacje | Tak (bez sekretów) |
| Nadpisania lokalne | .env.local, config/local.json |
Osoby indywidualne | Nie (ignore w git) |
Kolejność scalania: wartości domyślne → środowisko → lokalne, przy czym każda późniejsza warstwa nadpisuje poprzednią. Scalaj tylko tak głęboko, jak wymaga biznes; nie scalaj głęboko na ślepo — głębokie scalanie sprawia, że problemy takie jak „tablice niespodziewanie się skleciły” są ekstremalnie trudne do zauważenia.
/**
* 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. Waliduj przy starcie, nie czekaj do czasu wykonania
Koszt błędu konfiguracji rośnie wykładniczo z tym, jak późno zostanie odkryty:
| Kiedy zostanie odkryty | Koszt |
|---|---|
| Przy starcie | Jedna linia logu, restart i dalej |
| Przy pierwszym żądaniu | Część funkcji błędnie się kończy, godzina debugowania |
| Gdy odpali się zadanie harmonogramu | Alerty w środku nocy |
| W dniu rozliczenia | Incydent uzgodnieniowy |
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>;
}
Zasady wspierające:
- Kończ natychmiast, gdy brakuje wymaganego elementu; nie dostarczaj „wyglądającej sensownie” wartości domyślnej (takiej jak domyślenie adresu bazy na
localhost); - Kończ natychmiast przy błędach typu; łańcuch
"false"to nie wartość logicznafalse, więc zmienne środowiskowe trzeba konwertować jawnie po odczycie; - Sekrety nigdy nie trafiają do repozytorium; czytaj je wyłącznie ze zmiennych środowiskowych lub usługi zarządzania sekretami.
3. Który format wybrać dla plików konfiguracyjnych
| Plik | Format | Powód |
|---|---|---|
package.json |
Standardowy JSON | Menedżer pakietów akceptuje tylko ten |
tsconfig.json |
JSONC | Komentarze dozwolone, oficjalnie wspierane |
.vscode/settings.json |
JSONC | Jak powyżej |
| Konfiguracja biznesowa | YAML lub JSONC | Wymaga komentarzy wyjaśniających, co znaczy każdy element |
| Lista zmiennych środowiskowych | .env |
Płaskie pary klucz-wartość, łatwe do wstrzyknięcia |
Praktyczny test: czy ten plik będzie czytany przez narzędzia nieludzkie? Jeśli tak → standardowy JSON; jeśli czyta go tylko twój własny kod → dialekt jest w porządku.
4. Dane testowe: katalogi i nazewnictwo
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
Dobra konwencja nazewnicza to obiekt-scenariusz: order-missing-field jest znacznie bardziej użyteczne niż test1.json.
Nazwy plików z znakami spoza ASCII czasami powodują problemy z kodowaniem w wieloplatformowych łańcuchach narzędzi, więc trzymaj nazwy plików po angielsku, a treść może pozostać po chińsku.
4.1 Trzy rodzaje przypadków, trzy rodzaje oczekiwań
| Katalog | Oczekiwanie | Cel |
|---|---|---|
valid/ |
Walidacja przechodzi | Główny przepływ, generowanie typów |
invalid/ |
Walidacja zawodzi, a błąd wskazuje oczekiwane pole | Obsługa błędów, treść komunikatów |
edge/ |
Zachowanie jest jawne (przejście lub porażka są w porządku, ale muszą być asercowane) | Ochrona przed regresją |
JSONTestSuite używa prefiksów y_ / n_ / i_ dla tych samych trzech rodzajów (powinno akceptować / powinno odrzucać / zależne od implementacji),
i możesz tę konwencję nazewniczą zapożyczyć bezpośrednio.
5. Determinizm: uczyń dane testowe diffowalne
Niestabilne dane testowe czynią przegląd kodu bezużytecznym:
| Źródło szumu | Jak to obsłużyć |
|---|---|
| Aktualny czas | Ustal go na stałą, np. 2026-01-01T00:00:00Z |
| Losowe identyfikatory / UUID | Użyj rosnącej sekwencji lub stałych wartości |
| Kolejność kluczy | Sortuj alfabetycznie w czasie generowania |
| Formatowanie ułamków | Jednolicie zachowuj dwa miejsca lub konwertuj na łańcuchy |
| Strefa czasowa | Używaj wszędzie UTC |
| Wcięcia | Ustal na 2 spacje, sprawdzane przez Prettier w 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. Maskowanie i zgodność
Prawdziwych danych nie można używać bezpośrednio jako danych testowych:
- Imiona → placeholdery takie jak „John Doe” i „Jane Roe”;
- Numery telefonów → ewidentnie fikcyjne numery, takie jak
13800138000; - Numery dowodów → fałszywe numery o poprawnym formacie, ale niepoprawnej cyfrze kontrolnej;
- Adresy → zachowaj strukturę województwo/miasto, usuń numer domu;
- Kwoty → zachowaj rząd wielkości, ale nigdy nie zachowuj prawdziwych informacji o rachunku.
Najlepiej, aby zespół miał scripts/mask-fixture.mjs, który działa na wyeksportowanej produkcyjnej próbce i generuje zamaskowaną wersję,
unikając „zmień trzy miejsca ręcznie, zapomnij o reszcie”.
7. Migracja i wersje
Dane testowe też się starzeją. Dodaj pole schemaVersion do danych testowych albo umieść plik VERSION w katalogu:
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Gdy kontrakt API jest aktualizowany:
- Dodaj nowy katalog danych testowych (
v3/); nie modyfikuj starych danych w miejscu; - Zachowaj stare dane testowe do regresji, dopóki stara wersja API nie zostanie wycofana;
- Uruchamiaj obie wersje w CI, aby potwierdzić zgodność wsteczną.
8. Lista kontrolna
- [ ] Struktura konfiguracji trójwarstwowej jest jasna, a nadpisania lokalne nie są commitowane;
- [ ] Walidacja odbywa się przy starcie, a porażka kończy się natychmiast;
- [ ] Sekrety pochodzą wyłącznie ze zmiennych środowiskowych; pliki konfiguracyjne zawierają tylko placeholdery;
- [ ] Dane testowe są podzielone na katalogi
valid/invalid/edge, a nazwy wyrażają scenariusz; - [ ] Dane testowe są deterministyczne (stały czas, identyfikatory i kolejność) i diffowalne;
- [ ] Prawdziwe dane zostały przetworzone przez skrypt maskowania;
- [ ] Aktualizacje kontraktu dodają nowy katalog wersji, zamiast edytować w miejscu.
FAQ
Jak uporządkować i scalać warstwy konfiguracji?
Użyj trzech warstw — wartości domyślne, konfiguracja środowiska i nadpisania lokalne — scalając domyślne → środowisko → lokalne, przy czym każda późniejsza warstwa nadpisuje poprzednią. Scalaj tylko tak głęboko, jak wymaga biznes, i zastępuj tablice oraz skalary w całości, aby głębokie scalanie nie skleiło tablic niespodziewanie. Plik nadpisań lokalnych dodaj do gitignore, zamiast commitować osobiste ustawienia.
Dlaczego walidować konfigurację przy starcie?
Koszt błędu konfiguracji rośnie wykładniczo z tym, jak późno zostanie odkryty: jedna linia logu przy starcie versus incydent uzgodnieniowy w dniu rozliczenia. Waliduj względem JSON Schema przy starcie i kończ natychmiast przy brakujących wymaganych elementach lub błędach typu, zamiast dostarczać „wyglądającą sensownie” wartość domyślną.
Czy używać prawdziwych danych produkcyjnych bezpośrednio jako danych testowych?
Nie. Imiona, numery telefonów, numery dowodów, adresy i kwoty wymagają placeholderów lub ewidentnie fikcyjnych wartości, a zespół powinien mieć scripts/mask-fixture.mjs, który działa na produkcyjnym eksporcie i generuje zamaskowaną wersję, unikając ręcznych edycji, które coś pomijają.