JSON Toolkit

JSON Schema : des bases à la production

L’essentiel : Les mots-clés sont nombreux, mais une douzaine seulement servent au quotidien. Bien maîtriser type/properties/required/$ref, bien gérer les messages d'erreur, et vous couvrez quatre-vingts pour cent des cas réels.

Intermédiaire~14 min de lecturePublié le 2026-10-30Mis à jour le 2026-10-30
Illustration de l'article JSON Schema : des bases à la production

L'essentiel : fixez d'abord $schema sur 2020-12, puis extrayez les structures communes avec $defs + $ref ; n'utilisez oneOf que lorsqu'exactement une branche doit correspondre, et préférez les discriminateurs ou anyOf partout ailleurs.

Un schéma n'est pas une annexe de la documentation ; c'est un actif qui peut être exécuté, testé et généré.

1. Mettre d'abord de l'ordre dans les versions

Version Valeur de $schema Statut Remarques
draft-04 http://json-schema.org/draft-04/schema# Obsolète Projets hérités et certains outils OpenAPI 2.0
draft-07 http://json-schema.org/draft-07/schema# Très répandu Base de facto d'OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema Disponible Introduit $defs et le vocabulaire d'annotation
2020-12 https://json-schema.org/draft/2020-12/schema Recommandé actuellement Introduit prefixItems, modifie la sémantique de items

Utilisez 2020-12 pour les nouveaux projets. Notez qu'en 2020-12 items ne contraint que les « éléments restants » — les déclarations de type tuple sont passées à prefixItems. C'est le piège le plus courant lors d'une migration depuis draft-07.

2. Un schéma minimal utilisable

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

Ce schéma couvre les quatre préoccupations les plus courantes : contraintes de type, champs obligatoires, limites de plage et contraintes sur les éléments de tableau.

3. Carte des mots-clés

3.1 Général

Mot-clé Rôle Attention
type Le type ; peut être un tableau "type": ["string", "null"] signifie nullable
enum Valeurs énumérées Les valeurs doivent être uniques
const Contrainte sur une valeur unique Utile pour des valeurs fixes comme un numéro de version
$ref Référence un autre schéma Référence le schéma entier et ne fusionne pas avec les mots-clés frères de l'objet référençant

3.2 Nombres

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. Notez qu'en draft-04 exclusiveMaximum était un booléen (utilisé avec maximum) ; ce n'est qu'à partir de draft-07 qu'il est devenu un mot-clé numérique autonome — ce qui fait échouer en mode strict beaucoup de « schémas copiés de vieilles docs ».

3.3 Chaînes

minLength, maxLength, pattern (une expression régulière ECMA-262), et format (une annotation).

minLength compte les points de code Unicode, ni les unités UTF-16 ni les octets. Valider avec string.length en JavaScript donnera un résultat différent.

3.4 Tableaux

Mot-clé Rôle
prefixItems La contrainte de type tuple de 2020-12, faisant correspondre les éléments un par un par index
items Une contrainte uniforme sur les éléments restants (2020-12)
contains + minContains/maxContains Au moins/au plus combien d'éléments correspondants doivent être présents
uniqueItems Les éléments doivent tous différer (comparaison profonde ; à éviter sur les grands tableaux)
minItems / maxItems Contraintes de longueur

3.5 Objets

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

Trois idées fausses très fréquentes :

  1. required exige seulement que la clé existe : {"name": ""} est valide. Pour rejeter une chaîne vide, il faut ajouter minLength: 1 ;
  2. additionalProperties: false amplifie les problèmes de compatibilité : un champ superflu envoyé par un client fait échouer toute la charge utile, donc faire évoluer l'API oblige chaque appelant à mettre à jour en même temps ;
  3. properties ne restreint pas les clés non déclarées — par défaut elles sont autorisées.

3.6 Composition logique

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
Mot-clé Sémantique Échec typique
allOf Tous doivent être satisfaits Plusieurs branches imposent des contraintes contradictoires sur le même champ
anyOf Au moins un doit être satisfait Le message d'erreur est difficile à rattacher à une branche précise
oneOf Exactement un doit être satisfait Les données correspondent à deux branches et échouent, alors qu'elles « semblent manifestement correctes »
not Ne doit pas être satisfait Rarement utilisé, et messages d'erreur les moins conviviaux

oneOf est le mot-clé le plus susceptible de produire des « données valides qui échouent à la validation ». Si les contraintes de branches se chevauchent (par exemple, les deux autorisent des champs supplémentaires), un même enregistrement correspond à plusieurs branches et oneOf échoue d'emblée. Dans ce cas, passez à anyOf, ou utilisez un champ discriminateur avec const dans les branches.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

Le style discriminateur est plus lisible que oneOf, et produit de meilleures erreurs.

4. Découpage et réutilisation

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs est la forme introduite en 2019-09 ; draft-07 utilise definitions ;
  • Les références entre fichiers utilisent des $id relatifs, mais ne comptez pas sur la récupération de schémas distants à l'exécution en production — regroupez-les plutôt comme fichiers statiques ;
  • Vous ne pouvez pas ajouter de contraintes à côté d'une référence (le comportement de $ref avec les mots-clés frères varie selon la version). Quand il faut composer, utilisez allOf.

5. Validation à l'exécution avec Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

Points clés :

  • Appelez compile une seule fois, au niveau supérieur du module ; recompiler à chaque requête transforme une validation de 10 ms en centaines de millisecondes ;
  • strict: true fait apparaître tôt les mots-clés mal orthographiés (par exemple minlength au lieu de minLength), donc cela vaut la peine de l'activer ;
  • Seul ajv-formats fait réellement entrer en vigueur format: "email" et format: "date-time" ;
  • 2020-12 exige le point d'entrée ajv/dist/2020 — n'utilisez pas l'export par défaut.

6. Comment concevoir les messages d'erreur

Ce que le validateur vous donne, c'est « quel mot-clé a échoué » ; ce que l'utilisateur veut, c'est « quel champ de saisie est faux ». Il vaut la peine d'ajouter une couche de correspondance :

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Convert Ajv errors into display-ready messages.
 * @param errors The list of Ajv errors
 * @returns An array of display messages
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} is missing the required field ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} is too short`;
    }
    return `${label} failed validation (${errorItem.keyword})`;
  });
}

Côté API, conservez une structure lisible par machine pour que le frontend puisse décider de la présentation :

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "Request data validation failed",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
  ]
}

7. Performances et pièges

  • Le coût de validation croît avec la complexité du schéma et la taille des données ; plusieurs branches oneOf provoquent une explosion combinatoire, très perceptible avec des milliers d'enregistrements ;
  • uniqueItems effectue une comparaison profonde en O(n²) sur le tableau ; pour les grands tableaux, dédupliquez plutôt dans la couche métier ;
  • Évitez les motifs à retour arrière catastrophique dans pattern (comme (a+)+$) ; c'est le point d'entrée classique du ReDoS ;
  • Quand additionalProperties: false est combiné avec allOf, les champs déclarés dans un sous-schéma sont jugés « supplémentaires » par le parent, donc il faut les fusionner en un seul schéma ou utiliser unevaluatedProperties.

8. Liste de vérification

  • [ ] Fixer 2020-12 et déclarer $schema en tête de fichier ;
  • [ ] Extraire les structures communes avec $defs + $ref au lieu de copier-coller ;
  • [ ] Préférer les discriminateurs à oneOf ;
  • [ ] Réutiliser le résultat de compile du validateur et activer le mode strict ;
  • [ ] Documenter clairement si format est appliqué ;
  • [ ] Les réponses d'erreur fournissent le chemin, le mot-clé et un message destiné à l'utilisateur ;
  • [ ] Mettre le schéma dans la CI : exécuter les cas de la suite de tests JSON Schema comme tests de régression.

FAQ

Comment choisir un draft JSON Schema ?

Utilisez 2020-12 pour les nouveaux projets et déclarez $schema en tête de fichier. Si le schéma entre dans OpenAPI 3.1, il doit aussi être en 2020-12, car 3.1 intègre directement cette version ; ne gardez draft-07 que pour les projets encore liés à d'anciennes chaînes d'outils. Les deux se comportent différemment pour items et pour les mots-clés frères de $ref, donc ne les mélangez pas dans un même ensemble de schémas.

Pourquoi additionalProperties: false amplifie-t-il les problèmes de compatibilité ?

Il traite toute clé non déclarée comme une erreur, donc un champ superflu envoyé par un client fait échouer toute la charge utile. Chaque appelant doit alors se mettre à jour en même temps dès que l'API évolue, sinon les anciens clients sont rejetés. Les schémas de type configuration devraient fermer explicitement les champs superflus, tandis que les API publiques doivent peser le coût de compatibilité.

Faut-il utiliser oneOf ou anyOf ?

N'utilisez oneOf que lorsque les données doivent correspondre à exactement une branche. Si les contraintes de branches peuvent être vraies simultanément, un même enregistrement correspond à plusieurs branches et oneOf échoue d'emblée ; passez à anyOf, ou ajoutez un discriminateur const dans chaque branche.

Références

Articles liés

Points clés pour concevoir des contrats de données d'API JSON

Six choses sont très difficiles à changer une fois livrées : les enveloppes de réponse, les formats d'erreur, la forme de pagination, la représentation des identifiants et du temps, la sémantique de null contre absent, et l'évolution de version.

Intermédiaire~13 min de lectureMis à jour le 2026-10-30

Sans additionalProperties, la validation de schéma ne valide rien

Une validation réussie ne garantit pas des données propres. Pydantic ignore les champs inconnus par défaut, Zod les supprime, Ajv les laisse passer — les paramètres hallucinés et les clés mal orthographiées traversent en silence. Cet article montre les trois formes d'acceptation silencieuse et donne les recettes complètes : fermer les objets, contrôler la coercition, serrer chaque niveau d'imbrication.

Intermediate~8 min de lectureMis à jour le 2026-11-02