JSON Toolkit

Quand les erreurs JSON Schema n'ont aucun sens : localisation et gouvernance des messages Ajv

L’essentiel : Un validateur émet des erreurs lisibles par machine ; l'utilisateur veut savoir quel champ de saisie est faux. Couvre le tableau errors d'Ajv, instancePath contre schemaPath, le regroupement et la rédaction.

Avancé~10 min de lecturePublié le 2026-10-31Mis à jour le 2026-10-31
Schéma illustrant la localisation des erreurs de validation Ajv et la gouvernance des messages

L'essentiel : lisez d'abord instancePath pour localiser les données fautives, puis schemaPath pour localiser la règle déclenchée ; pour required et additionalProperties, extrayez le vrai nom de clé depuis params avant de choisir le message.

Le travail du validateur est de dire quelle règle a échoué. Le traduire en « ce champ de saisie est faux » est votre travail.

1. Ce que le tableau errors vous donne réellement

Ajv ne lève pas d'exception en cas d'échec de validation ; il laisse le résultat dans validate.errors. La configuration par défaut ne conserve que la première erreur, et allErrors est ce qui produit une liste complète. Un objet d'erreur unique a cinq champs qui comptent, et les comprendre est la prémisse de tout le reste :

Champ Signification Exemple
instancePath JSON Pointer vers la valeur fautive /email
schemaPath Emplacement du mot-clé fautif dans le schéma #/properties/email/format
keyword Nom de la contrainte violée format
params Paramètres lisibles par machine de cette contrainte { "format": "email" }
message Prose anglaise qui change entre versions must match format "email"

Une erreur réelle ressemble à ceci :

[
  {
    "instancePath": "/email",
    "schemaPath": "#/properties/email/format",
    "keyword": "format",
    "params": { "format": "email" },
    "message": "must match format \"email\""
  }
]

Notez que message est une prose écrite pour les humains, pas une interface stable. Ajv peut la reformuler entre versions, et elle ne peut pas servir un produit localisé. Ne la parsez jamais avec une expression régulière, et ne la renvoyez jamais telle quelle à un client.

2. instancePath et schemaPath répondent à des questions différentes

instancePath indique quelle tranche de données a échoué ; schemaPath indique quelle règle s'est déclenchée. Ce sont deux systèmes de coordonnées, et ils ne sont pas symétriques :

  • instancePath est un JSON Pointer RFC 6901. Une chaîne vide signifie la racine ; /items/0/price signifie le champ price du premier élément de tableau.
  • schemaPath commence par # et pointe le mot-clé dans le document de schéma. À travers un $ref, il pointe dans le schéma référencé, donc il vous faut le $id pour savoir de quel fichier il vient.
  • Une clé contenant / ou ~ est échappée en ~1 et ~0 respectivement. Annulez cet échappement avant l'affichage, sinon l'interface montrera des symboles étranges.

Par exemple, instancePath: "/email" avec schemaPath: "#/properties/email/format" signifie que le champ email de l'objet racine n'a pas passé la vérification de format.

Le piège le plus courant concerne required et additionalProperties : leur instancePath pointe l'objet parent, pas la clé fautive.

Mot-clé instancePath pointe vers Où vit la vraie clé
required L'objet parent (chaîne vide à la racine) params.missingProperty
additionalProperties L'objet parent params.additionalProperty
type La valeur fautive elle-même —
enum La valeur fautive elle-même params.allowedValues

Quand vous mappez les erreurs sur des champs de formulaire, traitez d'abord ces deux cas. Sinon « l'email est manquant » devient « erreur de l'objet racine », et l'utilisateur ne sait pas où taper.

3. allErrors et verbose : exhaustivité contre coût

Le défaut allErrors: false fait s'arrêter Ajv à la première défaillance. C'est bien pour un outil en ligne de commande et désastreux pour un formulaire : l'utilisateur corrige un champ, soumet, voit l'erreur suivante, et répète trois fois avant que le formulaire passe.

Réglage Comportement Bénéfice Coût
allErrors: false Seulement la première erreur Le plus rapide, mémoire minimale Soumissions répétées
allErrors: true Collecter toutes les erreurs Tous les problèmes en une passe Le nombre d'erreurs explose sur les schémas complexes
verbose: true Attacher schema et data à chaque erreur Diagnostic serveur plus facile Taille très accrue ; ne jamais l'exposer
messages: false Supprimer le champ message Charges utiles plus petites Perdre l'indice de débogage anglais

Répartition recommandée : activez allErrors: true pour la validation serveur et conservez le tableau complet dans les journaux internes, tandis que la réponse publique ne porte que des champs en liste blanche. N'activez verbose que pendant l'investigation ; il incorpore le fragment de schéma correspondant et les données de l'utilisateur dans chaque erreur, ce qui coûte de la mémoire et peut faire fuiter des valeurs sensibles dans les journaux.

Le volume d'erreurs est l'autre problème courant. Une branche anyOf échouée émet plusieurs erreurs enfants plus une erreur de synthèse, et oneOf avec de nombreuses branches est pire. Affichez le tableau tel quel et les utilisateurs voient cinq messages quasi identiques pour un seul champ. Le regroupement doit se faire avant le rendu.

4. Regrouper les erreurs par champ

Le regroupement fait trois choses : retransformer le chemin en nom de champ, corriger le décalage de required et additionalProperties, et fusionner plusieurs erreurs sur le même champ en un seul message le plus actionnable.

/** One validation error ready for display */
interface FieldError {
  field: string;
  keyword: string;
  params: Record<string, unknown>;
}

/**
 * Group Ajv errors by field.
 * @param errors Error array produced by Ajv
 * @returns Map keyed by field path, each value listing its errors
 */
function groupByField(errors: Array<Record<string, any>>): Record<string, FieldError[]> {
  const grouped: Record<string, FieldError[]> = {};

  for (const errorItem of errors) {
    const keyword: string = errorItem.keyword;
    let field: string = errorItem.instancePath;

    if (keyword === 'required') {
      const parentPath: string = errorItem.instancePath;
      field = `${parentPath}/${errorItem.params.missingProperty}`;
    } else if (keyword === 'additionalProperties') {
      field = `${errorItem.instancePath}/${errorItem.params.additionalProperty}`;
    }

    const normalizedField = field === '' ? '(root)' : field;
    (grouped[normalizedField] ??= []).push({
      field: normalizedField,
      keyword,
      params: errorItem.params ?? {},
    });
  }

  return grouped;
}

Fusionnez selon un ordre de priorité : required (manquant) d'abord, puis type, puis enum/const, puis format/pattern, puis longueur et plage. Affichez uniquement le message de plus haute priorité par champ et gardez le reste dans les journaux. La raison est simple : quand le type est faux, un message de longueur est du bruit, et corriger le type est la seule étape suivante sensée.

Les erreurs de synthèse de oneOf et anyOf ne portent généralement aucune information actionnable. Ignorez-les au rendu et ne gardez que les erreurs de branche qui pointent un champ concret.

5. Messages personnalisés : la voie du plug-in et la voie du gabarit

Ajv n'embarque aucune formulation localisée, donc deux options pratiques s'offrent à vous.

Option un : le plug-in ajv-errors. Il ajoute un mot-clé errorMessage pour que les messages vivent à côté des contraintes :

{
  "properties": {
    "email": {
      "type": "string",
      "format": "email",
      "errorMessage": {
        "format": "Please enter a valid email address",
        "type": "Email must be a string"
      }
    }
  }
}

L'avantage est que la formulation voyage avec le champ et que le schéma fait aussi office de documentation. L'inconvénient est que le texte est figé dans une langue, donc la localisation impose de maintenir plusieurs schémas. Le plug-in ne remplace aussi tous les messages que lorsque allErrors: true est défini ; oubliez ce drapeau et vous obtenez un mélange d'anglais personnalisé et par défaut.

Option deux : une table mot-clé vers gabarit. Le validateur ne fournit que keyword et params ; l'application produit la formulation :

Mot-clé Gabarit Paramètres disponibles
required Champ obligatoire manquant : {field} missingProperty
type Type attendu : {type} type
minLength Saisissez au moins {limit} caractères limit
maximum Ne doit pas dépasser {limit} limit
format Format invalide : {format} format
enum Doit être l'une des options autorisées allowedValues
pattern La valeur ne correspond pas au format attendu pattern (ne pas le reproduire)

L'option deux convient aux produits qui ont besoin de plusieurs langues et d'une voix cohérente. Elle sépare la validation de la présentation, garde la traduction hors du schéma, et permet au frontend de réutiliser les mêmes gabarits.

6. Localisation : les messages n'appartiennent pas au validateur

Déplacer les formulations dans des packs de langue est le point de départ de la gouvernance des messages. Un pack de langue stocke des gabarits et des clés, jamais des noms de champs ; les libellés de champs viennent de la configuration du formulaire ou du contrat d'API, et les deux sont combinés au rendu.

/** Validation message templates keyed by Ajv keyword */
const VALIDATION_TEMPLATES: Record<string, (params: Record<string, any>) => string> = {
  required: (params) => `Missing required field: ${params.missingProperty}`,
  type: (params) => `Expected type: ${params.type}`,
  minLength: (params) => `Enter at least ${params.limit} characters`,
  maximum: (params) => `Must not exceed ${params.limit}`,
  format: () => 'Invalid format',
};

/**
 * Build one user-facing validation message.
 * @param keyword Name of the failed constraint
 * @param params Constraint parameters
 * @returns Localized message text
 */
function buildMessage(keyword: string, params: Record<string, any>): string {
  const template = VALIDATION_TEMPLATES[keyword];
  return template ? template(params) : 'The value is invalid';
}

Trois règles comptent ici :

  1. Ne concaténez pas des phrases. « Email format is invalid » et son équivalent chinois ne partagent pas l'ordre des mots, et l'assemblage de chaînes finira par produire du grand n'importe quoi dans l'une des deux.
  2. Déléguez les pluriels et les classificateurs à la bibliothèque i18n. Ne codez jamais en dur « characters » dans un gabarit ; 1 character et 3 characters diffèrent.
  3. Traduisez aussi les libellés de champs. params.missingProperty vaut email, tandis que l'interface devrait dire « Adresse e-mail ». Gardez cette correspondance dans la configuration du formulaire, pas dans le pack de langue.

7. Ne renvoyez pas la structure interne aux clients

Renvoyer schemaPath, params et message ensemble est pratique en développement et une fuite d'information en production. allowedValues révèle les énumérations internes, pattern révèle les expressions régulières de validation, et schemaPath révèle l'arborescence des fichiers et le nommage des champs — dans certains cas, cela donne aux attaquants une carte de vos points faibles.

La réponse publique garde exactement trois choses : un chemin de champ (ou un nom de champ de formulaire), un code d'erreur stable et un texte destiné à l'utilisateur. Utilisez des codes courts et stables au lieu du keyword brut, pour conserver la possibilité de changer de validateur plus tard.

{
  "type": "about:blank",
  "title": "Request validation failed",
  "status": 422,
  "errors": [
    { "field": "email", "code": "invalid_format", "message": "Please enter a valid email address" },
    { "field": "items[0].price", "code": "invalid_pattern", "message": "Amount must have exactly two decimals" }
  ]
}

Les journaux serveur doivent contenir l'inverse : le tableau d'erreurs complet, l'identifiant de requête et la version du schéma, pour qu'une investigation puisse répondre « quelle règle a bloqué ceci ». Évitez de vider des corps de requête entiers dans les journaux ; gardez plutôt les chemins et mots-clés fautifs.

8. Présentation frontend : du surlignage de champ aux marqueurs d'éditeur

Les erreurs finissent dans une interface, et la présentation dépend du contexte :

Contexte Présentation Action clé
Formulaire Texte rouge sous le champ, focus sur la première erreur Mapper les chemins vers les noms de formulaire ; conserver les indices de tableau
Import de tableau en masse Numéro de ligne, nom de colonne, résumé, exportable Transformer /items/3/price en ligne 4, colonne prix
Éditeur JSON Marqueurs en ligne et panneau de problèmes Utiliser un parseur pour mapper le JSON Pointer en ligne et colonne
Outil de débogage d'API Liste d'erreurs structurée conservant schemaPath Destiné aux développeurs, donc plus de détail est acceptable

Trois notes d'implémentation :

  • N'affichez que la première erreur par champ, en repliant le reste dans « 2 autres problèmes », pour que le formulaire ne se noie pas dans le rouge.
  • Conservez les indices de tableau pour le ciblage mais supprimez-les dans les libellés. « Ligne 4, prix » se lit mieux que /items/3/price.
  • Les scénarios d'éditeur exigent un mapping de positions. Une simple recherche de chaîne est peu fiable quand un nom de champ se répète ; le parseur devrait fournir des offsets pour chaque nœud, que vous convertissez ensuite en ligne et colonne.

Si le JSON vient d'une saisie utilisateur plutôt que d'un formulaire, confirmez d'abord que le document est syntaxiquement valide. Les erreurs de syntaxe surviennent au parsing et n'atteignent jamais la validation de schéma. Analyser et formater JSON signale la ligne et la colonne exactes, ce qui est bien plus rapide que de courir après des erreurs de schéma. Pour comprendre comment des schémas trop stricts apparaissent quand ils sont inférés à partir d'échantillons, voir Pourquoi un JSON Schema généré automatiquement rejette sans cesse des données réelles ; pour la configuration du validateur et le système de mots-clés, voir JSON Schema : des bases à la production.

9. Questions fréquentes

Pourquoi ne reçois-je qu'une seule erreur ?

allErrors vaut false par défaut, donc Ajv renvoie à la première défaillance. Activez-le explicitement pour la validation de formulaire ; sinon les utilisateurs doivent soumettre plusieurs fois pour découvrir chaque problème.

Pourquoi le chemin d'une erreur required est-il vide ?

instancePath pour required pointe l'objet parent, qui est la chaîne vide à la racine ; le nom de la clé manquante se trouve dans params.missingProperty. Concaténez les deux pour obtenir le chemin complet. Afficher instancePath seul donne l'inutile « erreur de l'objet racine ».

Puis-je afficher message directement aux utilisateurs ?

Déconseillé. C'est de la prose anglaise dont la formulation change entre versions, et elle n'a aucune capacité de localisation. Générez plutôt le texte à partir de keyword et params, et réservez message aux journaux.

Pourquoi mon message d'erreur personnalisé est-il ignoré ?

Vérifiez d'abord que allErrors: true est activé ; ajv-errors ne peut remplacer tous les messages qu'avec ce drapeau. Confirmez ensuite que errorMessage est au même niveau que le mot-clé fautif — un niveau d'écart et il ne sera pas reconnu.

Quels champs peut-on renvoyer sans risque en production ?

Le chemin de champ, un code d'erreur stable et un texte destiné à l'utilisateur. Gardez schemaPath, params et message dans les journaux serveur : cela soutient le débogage sans exposer énumérations internes, expressions régulières ou arborescence de fichiers.