Gérer JSON pour la configuration, les fixtures et les données locales
L’essentiel : La configuration doit être stratifiée et validée ; les fixtures doivent être déterministes, lisibles et diffables. Si ces deux types de fichiers sont mal gérés, le coût de dépannage reste élevé longtemps.

L'essentiel : construisez la configuration comme défauts, environnement et surcharges locales avec validation au démarrage avant de vous soucier du format de fichier ; rendez les fixtures déterministes en temps, identifiants, ordre des clés et indentation pour qu'elles restent diffables, et seulement ensuite automatisez le masquage.
Ces deux types de JSON ne participent pas à la logique métier, et pourtant ils déterminent « combien de temps un nouveau collègue met à démarrer » et « combien de temps il faut pour localiser un incident de production ».
1. Configuration : une structure à trois couches
| Couche | Source | Maintenue par | Committée |
|---|---|---|---|
| Défauts | Constantes dans le code / default.json |
Développeurs | Oui |
| Configuration d'environnement | config/production.json, variables d'environnement |
Exploitation | Oui (sans secrets) |
| Surcharges locales | .env.local, config/local.json |
Individus | Non (gitignoré) |
Ordre de fusion : défauts → environnement → local, chaque couche ultérieure écrasant la précédente. Fusionnez seulement aussi profondément que le métier l'exige ; ne fusionnez pas profondément aveuglément — la fusion profonde rend des problèmes comme « les tableaux ont été concaténés par surprise » extrêmement difficiles à repérer.
/**
* 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. Validez au démarrage, n'attendez pas l'exécution
Le coût d'une erreur de configuration croît exponentiellement avec sa tardiveté de découverte :
| Quand elle est découverte | Coût |
|---|---|
| Au démarrage | Une ligne de journal, redémarrer et continuer |
| À la première requête | Quelques fonctionnalités échouent, une heure de débogage |
| Quand une tâche planifiée se déclenche | Alertes au milieu de la nuit |
| Le jour du règlement | Un incident de rapprochement |
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>;
}
Principes de soutien :
- Sortez immédiatement quand un élément obligatoire manque ; ne fournissez pas de défaut « plausible » (comme régler par défaut l'adresse de la base sur
localhost) ; - Sortez immédiatement en cas d'erreur de type ; la chaîne
"false"n'est pas le booléenfalse, donc les variables d'environnement doivent être converties explicitement après lecture ; - Les secrets ne vont jamais dans le dépôt ; lisez-les uniquement depuis les variables d'environnement ou un service de gestion de secrets.
3. Quel format utiliser pour les fichiers de configuration
| Fichier | Format | Raison |
|---|---|---|
package.json |
JSON standard | Le gestionnaire de paquets n'accepte que cela |
tsconfig.json |
JSONC | Commentaires autorisés, officiellement pris en charge |
.vscode/settings.json |
JSONC | Idem ci-dessus |
| Configuration métier | YAML ou JSONC | Besoin de commentaires expliquant chaque élément |
| Liste de variables d'environnement | .env |
Paires clé-valeur plates, faciles à injecter |
Un test pratique : ce fichier sera-t-il lu par des outils non humains ? Si oui → JSON standard ; si seul votre propre code le lit → un dialecte convient.
4. Fixtures de test : répertoires et nommage
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
Une bonne convention de nommage est objet-scénario : order-missing-field est bien plus utile que test1.json.
Les noms de fichiers non ASCII causent parfois des problèmes d'encodage dans les chaînes d'outils multi-plateformes, donc gardez les noms de fichiers en anglais tandis que le contenu peut rester en chinois.
4.1 Trois types de cas, trois types d'attentes
| Répertoire | Attente | But |
|---|---|---|
valid/ |
La validation passe | Flux principal, génération de types |
invalid/ |
La validation échoue, et l'erreur pointe le champ attendu | Gestion d'erreurs, copie des messages |
edge/ |
Le comportement est explicite (passer ou échouer convient, mais doit être asserté) | Protection contre les régressions |
JSONTestSuite utilise les préfixes y_ / n_ / i_ pour les mêmes trois types (devrait accepter / devrait rejeter / défini par l'implémentation),
et vous pouvez emprunter directement cette convention de nommage.
5. Déterminisme : rendez les fixtures diffables
Des fixtures instables rendent la revue de code inutile :
| Source de bruit | Comment la gérer |
|---|---|
| Heure courante | Fixez-la à une constante, par ex. 2026-01-01T00:00:00Z |
| Identifiants / UUID aléatoires | Utilisez une séquence incrémentale ou des valeurs fixes |
| Ordre des clés | Trier alphabétiquement au moment de la génération |
| Formatage des flottants | Gardez uniformément deux décimales ou convertissez en chaînes |
| Fuseau horaire | Utilisez UTC partout |
| Indentation | Fixe à 2 espaces, vérifiée par Prettier en 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. Masquage et conformité
Les données réelles ne peuvent pas être utilisées directement comme fixtures :
- Noms → placeholders comme « John Doe » et « Jane Roe » ;
- Numéros de téléphone → des numéros manifestement fictifs comme
13800138000; - Numéros d'identité → de faux numéros au format correct mais avec un chiffre de contrôle invalide ;
- Adresses → conservez la structure province/ville, supprimez le numéro de rue ;
- Montants → conservez l'ordre de grandeur, mais ne gardez jamais d'informations de compte réelles.
Il est préférable que l'équipe ait un scripts/mask-fixture.mjs qui tourne sur un échantillon de production exporté pour générer une version masquée,
évitant « changer trois endroits à la main, oublier le reste ».
7. Migration et versions
Les fixtures se périment aussi. Ajoutez un champ schemaVersion à la fixture, ou placez un fichier VERSION dans le répertoire :
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
Quand le contrat d'API est mis à niveau :
- Ajoutez un nouveau répertoire de fixtures (
v3/) ; ne modifiez pas les anciennes fixtures sur place ; - Conservez les anciennes fixtures pour la régression jusqu'au retrait de l'ancienne version d'API ;
- Exécutez les deux versions en CI pour confirmer la compatibilité descendante.
8. Liste de vérification
- [ ] La structure de configuration à trois couches est claire, et les surcharges locales ne sont pas committées ;
- [ ] La validation se fait au démarrage, et l'échec sort immédiatement ;
- [ ] Les secrets viennent uniquement des variables d'environnement ; les fichiers de configuration ne contiennent que des placeholders ;
- [ ] Les fixtures sont réparties en répertoires
valid/invalid/edge, et les noms expriment le scénario ; - [ ] Les fixtures sont déterministes (temps, identifiants et ordre fixes) et diffables ;
- [ ] Les données réelles ont été traitées par le script de masquage ;
- [ ] Les mises à niveau de contrat ajoutent un nouveau répertoire de version au lieu de modifier sur place.
FAQ
Comment ordonner et fusionner les couches de configuration ?
Utilisez trois couches — défauts, configuration d'environnement et surcharges locales — en fusionnant défauts → environnement → local, chaque couche ultérieure écrasant la précédente. Fusionnez seulement aussi profondément que le métier l'exige et remplacez tableaux et scalaires en bloc, pour que la fusion profonde ne puisse pas concaténer des tableaux par surprise. Gitignorez le fichier de surcharge locale plutôt que de committer des réglages personnels.
Pourquoi valider la configuration au démarrage ?
Le coût d'une erreur de configuration croît exponentiellement avec sa tardiveté de découverte : une ligne de journal au démarrage contre un incident de rapprochement le jour du règlement. Validez contre un JSON Schema au démarrage et sortez immédiatement en cas d'éléments obligatoires manquants ou d'erreurs de type, au lieu de fournir un défaut « plausible ».
Faut-il utiliser des données de production directement comme fixtures de test ?
Non. Noms, numéros de téléphone, numéros d'identité, adresses et montants ont tous besoin de placeholders ou de valeurs manifestement fictives, et l'équipe devrait avoir un scripts/mask-fixture.mjs qui tourne sur un export de production pour générer une version masquée, évitant des modifications manuelles qui ratent quelque chose.