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.

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 booleanofalse, 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:
- Aggiungi una nuova directory di fixture (
v3/); non modificare i vecchi fixture sul posto; - Mantieni i vecchi fixture per la regressione finché la vecchia versione dell'API non viene ritirata;
- 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.