JSON Toolkit

Gestire JSON per configurazione, fixture e dati locali

In sintesi: La configurazione deve essere stratificata e validata; i fixture devono essere deterministici, leggibili e diffabili. Se questi due tipi di file vengono gestiti male, i costi di troubleshooting restano alti a lungo.

Principiante~10 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Copertina di Gestire JSON per configurazione, fixture e dati locali

In sintesi: costruisci la configurazione come default, ambiente e override locali con validazione all'avvio prima di preoccuparti del formato del file; rendi i fixture deterministici in tempo, ID, ordine delle chiavi e indentazione così restano diffabili, e solo allora automatizza il mascheramento.

Questi due tipi di JSON non partecipano alla logica di business, ma determinano "quanto tempo impiega un nuovo collega a mettersi in funzione" e "quanto tempo ci vuole a localizzare un problema di produzione".

1. Configurazione: una struttura a tre strati

Strato Fonte Mantenuto da Committato
Default Costanti nel codice / default.json Sviluppatori Sì
Configurazione d'ambiente config/production.json, variabili d'ambiente Operazioni Sì (senza segreti)
Override locali .env.local, config/local.json Individui No (ignorati da git)

Ordine di merge: default → ambiente → locale, con ogni strato successivo che sovrascrive il precedente. Fai il merge solo profondo quanto serve al business; non fare deep merge alla cieca — il deep merge rende problemi come "gli array si sono concatenati inaspettatamente" estremamente difficili da individuare.

/**
 * 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. Valida all'avvio, non aspettare il runtime

Il costo di un errore di configurazione cresce esponenzialmente con quanto tardi viene scoperto:

Quando viene scoperto Costo
All'avvio Una riga di log, riavvia e vai avanti
Alla prima richiesta Alcune funzionalità danno errore, un'ora di debugging
Quando parte un job schedulato Avvisi nel cuore della notte
Il giorno del regolamento Un incidente di riconciliazione
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>;
}

Principi di supporto:

  • Esci subito quando manca un elemento obbligatorio; non fornire un default "plausibile" (come impostare l'indirizzo del database su localhost);
  • Esci subito sugli errori di tipo; la stringa "false" non è il booleano false, quindi le variabili d'ambiente devono essere convertite esplicitamente dopo la lettura;
  • I segreti non vanno mai nel repository; leggili solo da variabili d'ambiente o da un servizio di gestione dei segreti.

3. Quale formato usare per i file di configurazione

File Formato Motivo
package.json JSON standard Il package manager accetta solo questo
tsconfig.json JSONC Commenti ammessi, ufficialmente supportato
.vscode/settings.json JSONC Come sopra
Configurazione di business YAML o JSONC Necessita commenti che spieghino cosa significa ogni voce
Elenco di variabili d'ambiente .env Coppie chiave-valore piatte, facili da iniettare

Un test pratico: questo file verrà letto da strumenti non umani? Se sì → JSON standard; se solo il tuo codice lo legge → un dialetto va bene.

4. Fixture di test: directory e denominazione

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

Una buona convenzione di denominazione è oggetto-scenario: order-missing-field è molto più utile di test1.json. I nomi di file non ASCII causano occasionalmente problemi di codifica nelle toolchain cross-platform, quindi mantieni i nomi dei file in inglese mentre il contenuto può restare in cinese.

4.1 Tre tipi di casi, tre tipi di aspettative

Directory Aspettativa Scopo
valid/ La validazione passa Flusso principale, generazione dei tipi
invalid/ La validazione fallisce, e l'errore punta al campo previsto Gestione degli errori, testo dei messaggi
edge/ Il comportamento è esplicito (passare o fallire va bene, ma deve essere asserito) Protezione dalle regressioni

JSONTestSuite usa i prefissi y_ / n_ / i_ per gli stessi tre tipi (dovrebbe accettare / dovrebbe rifiutare / definito dall'implementazione), e puoi prendere in prestito direttamente quella convenzione di denominazione.

5. Determinismo: rendi i fixture diffabili

I fixture instabili rendono inutile la revisione del codice:

Fonte di rumore Come gestirla
Ora corrente Fissala a una costante, per esempio 2026-01-01T00:00:00Z
ID casuali / UUID Usa una sequenza incrementale o valori fissi
Ordine delle chiavi Ordina alfabeticamente al momento della generazione
Formattazione dei float Mantieni uniformemente due decimali o converti in stringhe
Fuso orario Usa UTC ovunque
Indentazione Fissa a 2 spazi, controllata da Prettier in 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. Mascheramento e conformità

I dati reali non possono essere usati direttamente come fixture:

  • Nomi → segnaposto come "John Doe" e "Jane Roe";
  • Numeri di telefono → numeri ovviamente fittizi come 13800138000;
  • Numeri di documento → numeri falsi con il formato corretto ma una cifra di controllo invalida;
  • Indirizzi → mantieni la struttura provincia/città, elimina il numero civico;
  • Importi → mantieni l'ordine di grandezza, ma non conservare mai informazioni di conti reali.

È meglio che la squadra abbia uno scripts/mask-fixture.mjs che gira su un campione di produzione esportato per generare una versione mascherata, evitando "cambio tre posti a mano, dimentico il resto".

7. Migrazione e versioni

Anche i fixture invecchiano. Aggiungi un campo schemaVersion al fixture, o metti un file VERSION nella directory:

{
  "schemaVersion": 3,
  "id": "1001",
  "amount": "89.50"
}

Quando il contratto API viene aggiornato:

  1. Aggiungi una nuova directory di fixture (v3/); non modificare i vecchi fixture sul posto;
  2. Mantieni i vecchi fixture per la regressione finché la vecchia versione dell'API non viene ritirata;
  3. Esegui entrambe le versioni in CI per confermare la retrocompatibilità.

8. Lista di controllo

  • [ ] La struttura a tre strati della configurazione è chiara, e gli override locali non vengono committati;
  • [ ] La validazione avviene all'avvio, e il fallimento esce subito;
  • [ ] I segreti provengono solo da variabili d'ambiente; i file di configurazione contengono solo segnaposto;
  • [ ] I fixture sono divisi nelle directory valid/invalid/edge, e i nomi esprimono lo scenario;
  • [ ] I fixture sono deterministici (tempo, ID e ordine fissi) e diffabili;
  • [ ] I dati reali sono stati elaborati dallo script di mascheramento;
  • [ ] Gli aggiornamenti del contratto aggiungono una nuova directory di versione invece di modificare sul posto.

FAQ

Come dovrebbero essere ordinate e unite le stratificazioni della configurazione?

Usa tre strati — default, configurazione d'ambiente e override locali — unendo default → ambiente → locale, con ogni strato successivo che sovrascrive il precedente. Fai il merge solo profondo quanto serve al business e sostituisci array e scalari per intero, così il deep merge non può concatenare array inaspettatamente. Ignora con git il file di override locale invece di committare impostazioni personali.

Perché validare la configurazione all'avvio?

Il costo di un errore di configurazione cresce esponenzialmente con quanto tardi viene scoperto: una riga di log all'avvio versus un incidente di riconciliazione il giorno del regolamento. Valida contro un JSON Schema all'avvio ed esci subito su elementi obbligatori mancanti o errori di tipo, invece di fornire un default "plausibile".

Devo usare i dati di produzione direttamente come fixture di test?

No. Nomi, numeri di telefono, numeri di documento, indirizzi e importi necessitano tutti di segnaposto o valori ovviamente fittizi, e la squadra dovrebbe avere uno scripts/mask-fixture.mjs che gira su un export di produzione per generare una versione mascherata, evitando modifiche manuali che dimenticano qualcosa.