JSON Toolkit

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.

Débutant~10 min de lecturePublié le 2026-10-30Mis à jour le 2026-10-30
Illustration de la gestion JSON pour la configuration, les fixtures et les données locales

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éen false, 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 :

  1. Ajoutez un nouveau répertoire de fixtures (v3/) ; ne modifiez pas les anciennes fixtures sur place ;
  2. Conservez les anciennes fixtures pour la régression jusqu'au retrait de l'ancienne version d'API ;
  3. 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.