Valider un fichier importé contre un schéma
Vérifier qu’un fichier JSON reçu d’un partenaire respecte le contrat convenu, et dire précisément ce qui ne va pas.
Pas besoin d’IA
GratuitInstantanéVos données ne sortent pas
Le verdict
N0, parce que le contrat est déjà exécutable et que le validateur est le même des deux côtés de l’échange. Trois choses valent d’être copiées de l’extrait, parce qu’aucune n’est le réglage par défaut : les formats sont assertés — sinon `jsonschema` accepte « 2026-02-30 » sans un mot, et `ajv` refuse de compiler le schéma —, toutes les erreurs sont collectées et non la première, et le schéma compilé est gardé, ce qui fait gagner deux ordres de grandeur en Python et davantage en JavaScript sur la mesure du relevé. Le point de rupture n’est pas dans le validateur mais dans le modèle de données : un montant décimal ne s’exprime pas dans un flottant binaire, et il s’écrit en centimes entiers.
JSON Schema 2020-12, formats assertés, toutes les erreurs rendues
Pas besoin d’IA
Règle et algorithme classiqueN0GratuitInstantané
Python
"""
Valider un document importé contre le schéma qu'il est censé suivre.
Niveau N0. JSON Schema est une norme publiée, et un validateur est un
programme qui l'applique : chaque échec revient avec l'endroit où il s'est
produit et la règle qu'il a enfreinte. Il n'y a rien à deviner et rien à
apprendre — un schéma est le contrat, écrit une fois, et le même document se
valide toujours de la même façon.
`jsonschema` l'applique ici, `ajv` en JavaScript. Deux choses qu'ils font tous les
deux et qui surprennent, et que ce fichier règle au lieu d'en hériter.
`format` est une annotation, pas une assertion. Les deux bibliothèques y
réagissent différemment, et les deux réactions surprennent quelqu'un :
`jsonschema` accepte « 12/01/2026 » et « 2026-02-30 » contre
`"format": "date"` sans un mot, tandis qu'`ajv` refuse purement et simplement
de compiler le schéma tant que les formats ne sont pas enregistrés. Les
enregistrer, plus bas, est ce qui met les deux d'accord — et fait refuser ces
deux dates aux deux.
Et un validateur s'arrête au premier échec tant qu'on ne lui dit pas le
contraire. Un import se vérifie une fois et se corrige une fois : toutes les
erreurs sont donc collectées, plafonnées, et le rapport dit combien ont été
laissées de côté plutôt que de faire comme s'il n'y en avait pas.
Ce qui revient, c'est un chemin et un nom de règle par échec. La formulation
du message appartient à la bibliothèque et diffère entre les deux ; le chemin
et la règle sont ce sur quoi votre code doit se brancher.
"""
from __future__ import annotations
import json
from jsonschema import Draft202012Validator, FormatChecker
# A file with one systematic mistake produces one error per record. Two hundred
# is enough to see the pattern; the count says how many more there were.
MAX_ERRORS = 200
# Compiling a schema costs far more than checking one document against it, and
# an import checks thousands against the same schema. The compiled form is kept
# here, keyed by the schema itself; the cap is what stops a caller that builds
# a new schema per record from filling memory.
_COMPILED: dict[str, object] = {}
MAX_COMPILED = 32
def validate_import(document, schema) -> dict:
"""
Check `document` against `schema`, and return every failure.
`errors` carries, for each one, the JSON Pointer of the value and the name
of the rule it broke — the two things a caller can act on — plus the
library's own message, which is for a human to read and not for code to
match on.
A schema that is itself invalid is reported as such rather than raised:
on an import path, a broken contract is an incident to log, not a crash.
"""
validator = _compiled(schema)
if validator is None:
return _report(False, [], 0, "this schema is not usable")
try:
failures = sorted(validator.iter_errors(document), key=_order)
except Exception: # noqa: BLE001 - a broken contract is an answer too
return _report(False, [], 0, "this schema is not usable")
kept = [{"path": _pointer(error.absolute_path),
"rule": str(error.validator),
"message": error.message}
for error in failures[:MAX_ERRORS]]
return _report(not failures, kept, len(failures), None)
def _compiled(schema):
"""The compiled validator for this schema, or None when it is not usable."""
try:
key = json.dumps(schema, sort_keys=True)
except TypeError:
return None
if key not in _COMPILED:
try:
# The schema itself is checked first: a rule the standard does not
# define — « objet » for « object » — would otherwise only surface
# as an exception in the middle of a file, on the first value that
# uses it.
Draft202012Validator.check_schema(schema)
_COMPILED[key] = Draft202012Validator(schema, format_checker=FormatChecker())
except Exception: # noqa: BLE001 - a broken contract is an answer too
_COMPILED[key] = None
if len(_COMPILED) > MAX_COMPILED:
del _COMPILED[next(iter(_COMPILED))]
return _COMPILED[key]
def _order(error):
"""
A single order, in both languages: by place in the document, then by rule.
An index sorts as a number, so record 10 comes after record 2 and not
after record 1 as a string comparison would have it.
"""
steps = [(0, step, "") if isinstance(step, int) else (1, 0, str(step))
for step in error.absolute_path]
return (steps, str(error.validator))
def _pointer(path) -> str:
"""The JSON Pointer of a value, which is what both libraries can produce."""
return "".join(f"/{str(step).replace('~', '~0').replace('/', '~1')}" for step in path)
def _report(valid: bool, errors: list, total: int, reason: str | None) -> dict:
return {"valid": valid, "errors": errors, "error_count": total,
"truncated": total > len(errors), "reason": reason}JavaScript
/**
* Valider un document importé contre le schéma qu'il est censé suivre.
*
* Niveau N0. JSON Schema est une norme publiée, et un validateur est un
* programme qui l'applique : chaque échec revient avec l'endroit où il s'est
* produit et la règle qu'il a enfreinte. Il n'y a rien à deviner et rien à
* apprendre — un schéma est le contrat, écrit une fois, et le même document se
* valide toujours de la même façon.
*
* `ajv` l'applique ici, `jsonschema` en Python. Deux choses qu'ils font tous les
* deux et qui surprennent, et que ce fichier règle au lieu d'en hériter.
*
* `format` est une annotation, pas une assertion. Les deux bibliothèques y
* réagissent différemment, et les deux réactions surprennent quelqu'un :
* `jsonschema` accepte « 12/01/2026 » et « 2026-02-30 » contre
* `"format": "date"` sans un mot, tandis qu'`ajv` refuse purement et simplement
* de compiler le schéma tant que les formats ne sont pas enregistrés. Les
* enregistrer, plus bas, est ce qui met les deux d'accord — et fait refuser ces
* deux dates aux deux.
*
* Et un validateur s'arrête au premier échec tant qu'on ne lui dit pas le
* contraire. Un import se vérifie une fois et se corrige une fois : toutes les
* erreurs sont donc collectées, plafonnées, et le rapport dit combien ont été
* laissées de côté plutôt que de faire comme s'il n'y en avait pas.
*
* Ce qui revient, c'est un chemin et un nom de règle par échec. La formulation
* du message appartient à la bibliothèque et diffère entre les deux ; le chemin
* et la règle sont ce sur quoi votre code doit se brancher.
*/
// `ajv` still defaults to draft-07; the 2020-12 dialect, the current one, is
// a separate entry point. Importing the default one instead refuses every
// schema that declares 2020-12, which is the shape the standard now publishes.
import AjvModule from 'ajv/dist/2020.js';
import addFormatsModule from 'ajv-formats';
// Both packages are published as CommonJS and carry their class on `default`.
const Ajv = AjvModule.default ?? AjvModule;
const addFormats = addFormatsModule.default ?? addFormatsModule;
// A file with one systematic mistake produces one error per record. Two
// hundred is enough to see the pattern; the count says how many more there were.
export const MAX_ERRORS = 200;
// Compiling a schema costs far more than checking one document against it, and
// an import checks thousands against the same schema. The compiled form is
// kept here, keyed by the schema itself; the cap is what stops a caller that
// builds a new schema per record from filling memory.
const COMPILED = new Map();
export const MAX_COMPILED = 32;
/** The compiled validator for this schema, or null when it is not usable. */
function compiled(schema) {
let key;
try {
key = JSON.stringify(schema) ?? 'undefined';
} catch {
return null;
}
if (!COMPILED.has(key)) {
let validate = null;
try {
// The schema itself is checked first: a rule the standard does not
// define — « objet » for « object » — would otherwise only surface as an
// exception in the middle of a file, on the first value that uses it.
const ajv = new Ajv({ allErrors: true, strictSchema: false });
addFormats(ajv);
validate = ajv.compile(schema);
} catch {
validate = null;
}
COMPILED.set(key, validate);
if (COMPILED.size > MAX_COMPILED) COMPILED.delete(COMPILED.keys().next().value);
}
return COMPILED.get(key);
}
/**
* Check `document` against `schema`, and return every failure.
*
* `errors` carries, for each one, the JSON Pointer of the value and the name
* of the rule it broke — the two things a caller can act on — plus the
* library's own message, which is for a human to read and not for code to
* match on.
*
* A schema that is itself invalid is reported as such rather than thrown: on
* an import path, a broken contract is an incident to log, not a crash.
*
* @param {unknown} document
* @param {object} schema
*/
export function validateImport(document, schema) {
const validate = compiled(schema);
if (!validate) return report(false, [], 0, 'this schema is not usable');
validate(document);
const failures = [...(validate.errors ?? [])].sort(order);
const kept = failures.slice(0, MAX_ERRORS).map((error) => ({
path: error.instancePath,
rule: error.keyword,
message: error.message,
}));
return report(failures.length === 0, kept, failures.length, null);
}
/**
* A single order, in both languages: by place in the document, then by rule.
* An index sorts as a number, so record 10 comes after record 2 and not after
* record 1 as a string comparison would have it.
*/
function order(a, b) {
const steps = (error) => error.instancePath.split('/').slice(1)
.map((step) => step.replace(/~1/g, '/').replace(/~0/g, '~'))
.map((step) => (/^\d+$/.test(step) ? [0, Number(step), ''] : [1, 0, step]));
const left = steps(a);
const right = steps(b);
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
if (i >= left.length) return -1;
if (i >= right.length) return 1;
for (let k = 0; k < 3; k += 1) {
if (left[i][k] < right[i][k]) return -1;
if (left[i][k] > right[i][k]) return 1;
}
}
return a.keyword < b.keyword ? -1 : a.keyword > b.keyword ? 1 : 0;
}
const report = (valid, errors, total, reason) => ({
valid, errors, error_count: total, truncated: total > errors.length, reason,
});Essayer
Ce code ne peut pas tourner dans un navigateur. Les sorties ci-dessous sont calculées à la construction du site, en exécutant le vrai code.
Le même contrat d’import pour tous les cas, sauf le dernier qui n’a qu’un montant. Les sorties sont celles de l’extrait, exécutées à la construction du site.
Une facture conforme au contrat
Donné
Rien à signaler : les quatre champs sont là et bien typés.
Ce qu’il renvoie
{ "reference": "FA-000123", "montant": 1250, "date": "2026-01-12", "client": "Boulangerie Martin" }
Ce qu’il en conclut
Valide
Rien à signaler : les quatre champs sont là et bien typés.
Le détail
Chemin Règle Message Un nom de champ mal orthographié
Donné
« montnat » au lieu de « montant » : le schéma fermé le nomme au lieu de le perdre.
Ce qu’il renvoie
{ "reference": "FA-000123", "montnat": 1250, "montant": 0, "date": "2026-01-12" }
Ce qu’il en conclut
1 erreur(s)
« montnat » au lieu de « montant » : le schéma fermé le nomme au lieu de le perdre.
Le détail
Chemin Règle Message (la racine) additionalProperties must NOT have additional properties Une date qui n’existe pas
Donné
Le 30 février : refusé, parce que le contrôle de format est allumé.
Ce qu’il renvoie
{ "reference": "FA-000123", "montant": 1250, "date": "2026-02-30" }
Ce qu’il en conclut
1 erreur(s)
Le 30 février : refusé, parce que le contrôle de format est allumé.
Le détail
Chemin Règle Message /date format must match format "date" Trois erreurs dans le même enregistrement
Donné
Toutes rendues, rangées par place dans le document, pas seulement la première.
Ce qu’il renvoie
{ "reference": "FA-12", "montant": "1 250,00", "date": "12/01/2026" }
Ce qu’il en conclut
3 erreur(s)
Toutes rendues, rangées par place dans le document, pas seulement la première.
Le détail
Chemin Règle Message /date format must match format "date" /montant type must be number /reference pattern must match pattern "^FA-[0-9]{6}$" Un montant ordinaire contre « multipleOf: 0.01 »
Donné
Le montant 19,99 : parfaitement ordinaire, et refusé.
Ce qu’il renvoie
19.99
Ce qu’il en conclut
1 erreur(s)
Le montant 19,99 : parfaitement ordinaire, et refusé.
Le détail
Chemin Règle Message (la racine) multipleOf must be multiple of 0.01 Là, ça casse« multipleOf: 0.01 » est la façon naturelle d’écrire « des euros et des centimes », et elle refuse 1 363 des 10 000 montants de 0,00 à 99,99 — dont 19,99, 4,35 et 1 234,56 — parce qu’un nombre JSON est un flottant binaire et que 0,01 ne s’y écrit pas exactement. La réponse n’est pas un autre validateur : c’est d’écrire le montant en centimes entiers, ce que les mêmes 10 000 valeurs passent sans une erreur.
Ce que ça implique
- Vos données ne sortent pas
- Résultat toujours identique
- Se teste unitairement, cas par cas
- Dépend d’une bibliothèque
- Empreinte négligeable
Ce que cela ajoute à votre périmètre réglementaire
- Aucun périmètre spécifique ajouté : le fichier est lu dans votre processus et n’est transmis à personne
- Le rapport d’erreurs porte les valeurs refusées : ce qu’il journalise est ce que contenait le fichier
Point de rupture
Un nombre JSON est un flottant binaire, et « multipleOf: 0.01 » — la façon naturelle d’écrire un montant en euros et centimes — refuse 1 363 des 10 000 montants de 0,00 à 99,99, dont 19,99, 4,35 et 1 234,56. Le témoin est dans le même test : les mêmes 10 000 valeurs, écrites en centimes entiers, passent toutes.
Quand passer au niveau suivant
Il n’y a pas de niveau au-dessus. Le jour où la règle à vérifier n’est plus dans le fichier — ce SIREN existe-t-il, ce client est-il à jour de ses paiements —, ce n’est plus une validation de schéma, c’est une consultation.
Les quatre options
- RecommandéN0JSON Schema 2020-12, formats assertés, toutes les erreurs renduesGratuit · Instantané
- N1Niveau sans objet ici
- N2Niveau sans objet ici
- N3Niveau sans objet ici
Les autres options
N1Niveau sans objet ici
Niveau sans objet ici
Modèle classique légerN1
Il n’y a rien à apprendre : le schéma est le savoir, écrit à la main parce que c’est un accord entre deux parties, et le validateur ne fait que l’appliquer. Un modèle classique entraîné sur des fichiers étiquetés valides ou non apprendrait la forme des fichiers que vous avez déjà reçus, ce qui est l’inverse du besoin : un contrat sert justement à refuser ce qu’on n’a jamais vu.
N2Niveau sans objet ici
Niveau sans objet ici
Petit modèle spécialisé auto-hébergéN2
Même argument, et une objection de forme : ce niveau doit rendre un chemin exact — « /4321/montant » — et le nom d’une règle, parce que c’est ce qu’un opérateur corrige. Un petit modèle spécialisé rend un score sur un document, pas une adresse dans ce document, et il faudrait de toute façon écrire le schéma pour savoir sur quoi l’entraîner.
N3Niveau sans objet ici
Niveau sans objet ici
API de LLM généralisteN3
Un modèle généraliste sait lire un schéma et un document, et il rendra un avis vraisemblable sur leur accord. Ce que la fiche lui reproche est ailleurs : il faudrait lui envoyer le fichier — des données d’un partenaire, souvent personnelles —, payer un appel par enregistrement sur des lots de dizaines de milliers de lignes, et accepter que deux passages sur le même fichier puissent ne pas rendre la même liste d’erreurs. Le validateur, lui, est le même programme que celui du partenaire qui a produit le fichier.
Pour aller plus loin
- JSON Schema — la spécification 2020-12 et son vocabulaire de validation
- jsonschema — la bibliothèque Python
- ajv — la bibliothèque JavaScript, et ses points d’entrée par dialecte
- RFC 6901 — JSON Pointer, la façon de nommer une valeur dans un document
Source des chiffres
- JSON Schema 2020-12, section 7 (consultée) : « format » est un mot-clé d’annotation par défaut, et son assertion est laissée à l’implémentation
- jsonschema sur PyPI (consulté) : version 4.26.0, publiée le 7 janvier 2026, licence MIT
- ajv, documentation des dialectes (consultée) : le point d’entrée par défaut est draft-07, et le dialecte 2020-12 s’importe depuis « ajv/dist/2020 »
- ajv-formats sur npm (consulté) : version 3.0.1, publiée le 30 mars 2024, licence MIT ; c’est le paquet qui enregistre « date », « email » et les autres
Métadonnées
Information générale, tenue à jour au mieux. Ce n’est pas un avis juridique. Faites valider votre situation par un professionnel.