JSON Toolkit

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.

Początkujący~10 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Okładka artykułu o zarządzaniu JSON dla konfiguracji, danych testowych i danych lokalnych

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ść logiczna false, 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:

  1. Dodaj nowy katalog danych testowych (v3/); nie modyfikuj starych danych w miejscu;
  2. Zachowaj stare dane testowe do regresji, dopóki stara wersja API nie zostanie wycofana;
  3. 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ą.