Validation JSON en CI : un pipeline avec jsonlint, ajv et jq
L’essentiel : La validation syntaxique prouve qu'un fichier parse ; la validation de schéma prouve que les champs suivent le contrat. En CI, ce qui compte, ce sont des codes de sortie non ambigus, des localisations d'erreur et des chemins de réparation.

L'essentiel : en CI, laissez jq empty gérer la syntaxe, ajv-cli gérer la structure (check-jsonschema pour les équipes multi-format), et Prettier gérer le style ; enchaînez-les par couche et arrêtez-vous au premier échec.
Passer le parseur dans votre éditeur local ne signifie pas que le fichier appartient à la CI ; passer la CI ne signifie pas non plus que les champs correspondent au contrat.
1. Séparez la validation syntaxique de la validation de schéma
L'expression « validation JSON » est utilisée bien trop largement, c'est pourquoi les équipes choisissent souvent un outil pour résoudre deux problèmes différents :
| Couche | Question à laquelle elle répond | Outils typiques | Ce que signifie un échec |
|---|---|---|---|
| Syntaxe | Ce fichier est-il du JSON valide ? | jq empty, jsonlint, python -m json.tool |
Crochets, virgules, guillemets ou encodage sont cassés |
| Schéma | Les champs, types et propriétés obligatoires correspondent-ils au contrat ? | ajv-cli, check-jsonschema |
Le fichier parse, mais la structure métier est fausse |
| Style | L'indentation et l'ordre des clés sont-ils cohérents ? | Prettier, jq -S |
Le formatage est incohérent et les diffs sont bruyants |
Ces couches ne peuvent pas se remplacer. Un fichier qui passe la syntaxe peut encore échouer au schéma : {"price": "12.5"} est du JSON valide, mais si le schéma exige que price soit un nombre, il n'est pas acceptable. Inversement, aussi complet que soit le schéma, un fichier auquel il manque une accolade fermante ne compilera jamais contre lui.
Donc le pipeline devrait exécuter la syntaxe d'abord, la structure ensuite, et le style en dernier. Arrêtez-vous dès qu'une couche échoue, sinon une erreur de parsing se déguise en erreur de champ.
2. Couche syntaxe : jq empty est l'option à dépendance minimale
Si jq est déjà dans l'image CI, la validation syntaxique ne coûte presque rien :
jq empty data.json
jq empty ne produit aucune sortie ; il parse seulement le fichier. Il sort avec 0 en cas de succès et un code non nul plus des informations de ligne/colonne en cas d'échec. Cela le rend plus léger que de tirer tout un arbre de dépendances Node, et c'est une bonne première barrière.
Lors d'une validation par lots de fichiers JSON dans un dépôt, excluez node_modules, la sortie de build et les caches tiers :
find . -name '*.json' \
-not -path './node_modules/*' \
-not -path './dist/*' \
-print0 | xargs -0 -n1 jq empty
-print0 et xargs -0 gèrent les espaces dans les noms de fichiers, tandis que -n1 garantit un fichier par invocation pour que le message d'erreur identifie le coupable. Ne passez pas des centaines de fichiers à un seul processus ; les messages d'erreur s'écraseraient.
jsonlint est un autre choix courant, généralement invoqué comme npx jsonlint -q data.json, où -q n'imprime que les erreurs. Il rapporte des numéros de ligne et colonne, mais sa maintenance est moins active que jq, et différentes versions tolèrent différemment le BOM et les commentaires. Traitez-le comme un vérificateur de syntaxe plus lisible, pas comme un validateur de schéma.
3. Couche schéma : les bases de ajv-cli validate
Ajv est le validateur de schéma le plus largement utilisé dans l'écosystème JavaScript, et ajv-cli l'expose comme outil en ligne de commande :
ajv validate -s schema.json -d data.json
-s pointe le fichier de schéma et -d les données à valider. En cas d'échec le processus sort non nul, donc la CI échoue immédiatement. Les vrais projets ajoutent généralement deux options :
npx ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
--strict=false aide avec les anciens schémas : le mode strict traite les mots-clés inconnus et les déclarations required suspectes comme des erreurs, ce que beaucoup de schémas hérités échouent. --errors=text produit une sortie lisible par l'humain, tandis que la structure JSON par défaut convient mieux aux machines. Choisissez selon qui consomme le résultat.
La version de draft du schéma doit aussi correspondre. Les versions majeures d'Ajv prennent en charge différents drafts par défaut, et ajv-cli peut en sélectionner un avec --spec=draft7, --spec=draft2019 ou --spec=draft2020. Si un schéma déclare $schema: "https://json-schema.org/draft/2020-12/schema" mais que le CLI le compile comme draft-07, certains mots-clés sont silencieusement ignorés. Pour en savoir plus sur la rédaction de schémas et les différences de version, voir Premiers pas avec JSON Schema.
La validation de format est une autre exigence fréquente. Le mot-clé format est une annotation seule par défaut ; pour que email, date-time et uri appliquent réellement quelque chose, chargez ajv-formats :
npx ajv-cli validate -c ajv-formats -s schema.json -d data.json
Cette étape est facile à manquer, et le résultat est qu'un champ email accepte not-an-email. Avant de livrer, fournissez au pipeline un enregistrement délibérément invalide ; cela prouve que la règle est active bien mieux que de lire dix fois la documentation.
check-jsonschema : formats multiples et erreurs plus conviviales
Si l'équipe traite du YAML, du TOML ou de la configuration GitHub Actions en plus du JSON, check-jsonschema est plus pratique :
check-jsonschema --schemafile schemas/config.json config/app.json
check-jsonschema --schemafile schemas/pipeline.json .github/workflows/ci.yml
Ses avantages pratiques :
- Support YAML natif, qui convient aux fichiers « en forme de JSON mais écrits en YAML », comme les manifests Kubernetes et la configuration CI.
- Les erreurs incluent le chemin de fichier, le numéro de ligne et le chemin de champ, gardant les coûts de localisation bas.
- Un hook pre-commit est fourni, donc l'intégration est peu coûteuse.
- Il peut tirer des schémas communs de SchemaStore, mais accepte aussi des fichiers locaux, évitant une dépendance réseau.
Dans les équipes multi-langages, Python est souvent déjà présent, donc installer et mettre à niveau check-jsonschema est plus léger que de maintenir une chaîne d'outils Node. Laissez jq gérer la syntaxe et cet outil gérer la structure ; la combinaison a une surcharge de maintenance très faible.
4. Les codes de sortie sont le seul signal CI fiable
Peu importe la beauté de la sortie, la CI ne lit que les codes de sortie. Trois pièges sont courants :
| Symptôme | Cause | Correction |
|---|---|---|
| Des erreurs existent mais la CI est verte | La commande finit par || true |
Supprimez le repli, ou utilisez-le seulement là où c'est explicitement autorisé |
| Aucune erreur mais la CI est rouge | diff traite « différences trouvées » comme un échec |
Distinguez « différence » de « erreur d'exécution » |
| La sortie d'erreur est tronquée | La sortie parallèle multi-fichiers s'écrase elle-même | Exécutez en série ou imprimez par fichier |
Retenez la sémantique des codes de sortie de jq : le parsing normal renvoie 0 ; avec -e, un résultat d'expression false ou null renvoie 1 ; les erreurs d'usage et les fichiers manquants renvoient 2 ; les erreurs de compilation de script renvoient 3. Les outils de schéma utilisent généralement 0 pour le succès, 1 pour un échec de validation, et 2 pour des erreurs d'outil ou d'argument. Lors de l'écriture d'un wrapper, ne réduisez pas chaque code non nul à « les données sont fausses », sinon un échec d'environnement sera diagnostiqué à tort comme un problème de contenu.
5. pre-commit : attraper les problèmes avant le commit
La valeur de pre-commit n'est pas « une vérification de plus » ; c'est de donner aux développeurs un retour en une minute au lieu d'après dix minutes de CI. Une configuration minimale ressemble à ceci :
repos:
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.0
hooks:
- id: check-jsonschema
files: ^configs/.*\.json$
args:
- --schemafile
- schemas/config.json
- repo: local
hooks:
- id: jq-syntax
name: jq syntax check
entry: jq empty
language: system
files: ^configs/.*\.json$
Deux détails comptent :
filesdevrait correspondre seulement aux répertoires qui ont besoin de validation. N'exécutez pas de hooks surnode_modulesou des fichiers générés.- Le hook local suppose que
jqest installé sur la machine du développeur. Documentez cette exigence, ou passez à un hook conteneurisé.
Si le projet utilise déjà Husky et lint-staged, combinez-les directement :
{
"lint-staged": {
"configs/*.json": ["jq empty", "prettier --check"]
}
}
Rappelez-vous que lint-staged passe une liste de fichiers indexés, donc la commande doit accepter plusieurs chemins. jq empty et prettier --check le font tous deux. Ne laissez pas un hook scanner tout le dépôt ; les commits deviendront nettement plus lents.
6. GitHub Actions : un pipeline copiable
Le workflow ci-dessous effectue la validation syntaxique, la validation de schéma et conserve le rapport en cas d'échec :
name: json-validate
on:
push:
paths:
- 'schemas/**'
- 'fixtures/**'
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Syntax check
run: |
find fixtures -name '*.json' -print0 | xargs -0 -n1 jq empty
- name: Schema check
run: |
npx --yes ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
Le filtre paths empêche les changements de documentation seule de déclencher la validation. npx --yes convient aux dépôts sans dépendances épinglées ; les projets de production devraient ajouter ajv-cli à devDependencies et épingler la version. Sans lock, une sortie amont qui change le draft ou le mode strict par défaut peut rendre le pipeline rouge du jour au lendemain.
7. Localisation des erreurs : des numéros de ligne aux chemins de champ
La moitié de la valeur d'un validateur est la détection ; l'autre moitié est la localisation. Apprenez à lire trois types de sortie :
- Erreurs de syntaxe :
jqrapporte ligne et colonne, souvent avec un extrait voisin. Corrigez d'abord crochets et virgules ; ne suspectez pas immédiatement l'encodage. - Erreurs de schéma :
instancePathd'Ajv pointe l'emplacement des données (par exemple/items/0/price),schemaPathpointe la règle (par exemple#/properties/items/items/properties/price/type), etkeywordnomme la contrainte violée. - Erreurs de format : quand une vérification
formatéchoue, le message dit souvent seulement « must match format "email" ». Combinez-le avecinstancePathpour identifier le champ.
Collez instancePath dans l'enregistrement d'investigation ; il est bien plus utile qu'un mur de JSON. Pour les tableaux, l'index numérique indique aussi quel enregistrement a échoué. Si la clé métier est plus intuitive, ajoutez ensuite une ligne de correspondance, comme items[0].id = ORD-042.
Les erreurs de schéma courantes, comme l'abus de additionalProperties: false ou des branches oneOf qui se chevauchent réellement, sont couvertes dans Pièges courants de génération de JSON Schema. Une règle de validation fausse est plus dangereuse qu'aucune validation, car elle crée un faux sentiment de sécurité.
8. Tableau de sélection d'outils
| Besoin | Outil recommandé | Pourquoi | Attention |
|---|---|---|---|
| Syntaxe seulement | jq empty |
Zéro dépendance, rapide, codes de sortie clairs | Espaces dans les noms de fichiers lors des exécutions par lots |
| Erreurs ligne/colonne plus conviviales | jsonlint |
Sortie lisible | Activité de maintenance limitée |
| Validation JSON Schema | ajv-cli |
Écosystème mature, erreurs structurées | Aligner les versions de draft et charger le support de format |
| Dépôt mixte JSON et YAML | check-jsonschema |
Multi-format, convivial pour pre-commit | Source du schéma et accès réseau |
| Retour rapide avant commit | pre-commit / lint-staged | Ne traite que les fichiers indexés | Les hooks doivent accepter plusieurs chemins |
| Formatage cohérent | Prettier | Largement adopté | Utilisez --check quand vous ne voulez pas réécrire les fichiers |
Une recommandation finale : vérifiez que le pipeline échoue réellement en lui fournissant d'abord un enregistrement délibérément invalide. Le plus grand risque n'est pas une configuration compliquée ; c'est une configuration qui ne prend jamais effet. Traiter « il peut échouer » comme critère d'acceptation est plus fiable que de traiter « il passe » comme tel. Pour la chaîne d'outils en ligne de commande plus large, poursuivez avec Éditeurs, ligne de commande et automatisation.
FAQ
Faut-il utiliser jq empty ou jsonlint ?
Préférez jq empty en CI : zéro dépendance, démarrage rapide et sémantique de code de sortie claire. Utilisez jsonlint quand il faut des messages ligne/colonne plus conviviaux ou que vous investiguez localement. Les deux n'effectuent qu'une validation syntaxique ; aucun ne vérifie la structure des champs.
Pourquoi ajv signale-t-il un mot-clé inconnu ?
Confirmez d'abord que le draft $schema du schéma correspond à la version utilisée par le CLI. Beaucoup d'erreurs « mot-clé inconnu » sont des mots-clés de nouveau draft rencontrés par un compilateur plus ancien. Si les versions correspondent, cherchez une faute de frappe. --strict=false peut garder des schémas hérités fonctionnels, mais ne comptez pas dessus à long terme pour masquer de vrais problèmes.
La validation de schéma passe, mais l'API échoue quand même. Pourquoi ?
Un schéma valide la structure, pas la sémantique métier. Par exemple, status peut être une chaîne avec une énumération légale, tandis que la machine à états interdit de passer directement de paid à refunded. Ces règles appartiennent au code ou aux contraintes de base de données, pas à JSON Schema.
Comment localiser rapidement le champ fautif ?
Lisez l'instancePath ou le numéro de ligne dans la sortie de l'outil. L'instancePath d'Ajv est un JSON Pointer qui pointe directement les données ; check-jsonschema rapporte fichier et ligne. Enregistrez le chemin et mappez-le vers la clé métier ; localiser le problème prend généralement quelques minutes.
pre-commit doit-il lancer une validation complète ?
Non. pre-commit devrait traiter seulement les fichiers indexés pour garder les commits rapides. Les scans complets appartiennent à la CI ou à des tâches planifiées. La division est simple : les exécutions locales privilégient la vitesse, la CI privilégie la complétude.