JSON d’une IA : valider les types et la cohérence avec Pydantic

Illustration d’une carte JSON traversant des contrôles, accompagnée d’un point d’interrogation sur la fiabilité du contenu.

Valider un JSON d’IA ne consiste pas seulement à vérifier qu’il se lit avec json.loads. Une réponse peut avoir une syntaxe correcte, les bons champs et des types acceptables, tout en annonçant un total faux ou une date de livraison antérieure à la commande. Elle peut aussi respecter toutes ces règles et décrire un produit inventé.

Ce cas Python utilise Pydantic pour séparer quatre questions : le texte est-il du JSON, respecte-t-il le schéma, est-il cohérent selon nos règles, et ses informations sont-elles vraies ? Les onze réponses sont fabriquées pour l’exercice. Aucun modèle distant n’est appelé et aucune performance de LLM n’est mesurée.

Définir le contrat avant de demander une réponse structurée

Nous attendons une commande simplifiée avec une devise EUR, deux dates au format AAAA-MM-JJ, une liste non vide de lignes et un total en centimes. Chaque ligne porte un nom d’article, une quantité entière strictement positive et un prix unitaire entier positif ou nul. Les taxes, remises et frais ne font pas partie de ce contrat pédagogique.

Deux contraintes relient les champs : la livraison ne doit pas précéder la commande, et le total doit être égal à la somme des quantités multipliées par les prix. Un objet qui ne respecte pas ces règles est rejeté ; le validateur ne répare pas silencieusement la réponse.

Dans Pydantic, ConfigDict(strict=True, extra="forbid") est appliqué au modèle principal et aux lignes. Une quantité "2" sous forme de texte n’est pas acceptée comme entier. Un booléen true n’est pas accepté comme quantité. Un champ inattendu est également refusé.

Onze exemples : neuf rejets et deux acceptations à interpréter

Cas simulé Validation locale
Commande cohérente Accepté
Total 701 au lieu de 700 Rejeté
Quantité sous forme de texte Rejeté
Quantité booléenne Rejeté
Champ supplémentaire Rejeté
Livraison avant commande Rejeté
Date impossible Rejeté
Liste de lignes vide Rejeté
Clé JSON répétée Rejeté
Virgule finale invalide Rejeté
Produit non vérifié, objet cohérent Accepté

La commande cohérente contient deux cahiers à 350 centimes, pour un total de 700. Modifier seulement ce total en 701 conserve un JSON valide, mais viole notre règle métier. Une date impossible, une liste vide ou une livraison antérieure à la commande sont aussi rejetées.

Le dernier exemple est volontairement accepté : son nom d’article vaut produit-inexistant-dans-le-catalogue, mais il respecte le schéma et les calculs. Aucun catalogue n’est consulté. Ce résultat montre la limite du contrôle, pas un succès de vérification factuelle.

Syntaxe, types et cohérence : trois contrôles distincts

La première étape décode le JSON avec la bibliothèque standard. Nous lui ajoutons un contrôle des clés répétées : une réponse qui contient deux fois total_centimes est refusée, même si les deux valeurs sont identiques. Le contrôle s’applique aussi aux objets imbriqués. Les constantes NaN et infinies non conformes au JSON sont refusées.

La deuxième étape valide les champs et les types avec Pydantic. La troisième applique nos validateurs : le format des dates est explicite, leur existence est contrôlée avec date.fromisoformat, puis un model_validator compare la chronologie et recalcule le total.

Le mode strict de Pydantic évite des conversions implicites, mais son comportement dépend du type et de l’entrée Python ou JSON. Ici, nous décodons d’abord en objets Python et gardons les dates comme chaînes strictement contrôlées. La documentation des validateurs explique les contrôles au niveau des champs et du modèle.

Exécuter le validateur et ses tests

Télécharger le code et les fichiers — ZIP gratuit. Archive sans inscription, 7 tests, vérifiée après extraction. Dans un environnement Python dédié, installez python -m pip install -r requirements.txt, puis exécutez python validation_json.py et python test_exemple.py. Le programme fonctionne localement après installation et ne transmet aucune donnée.

Le téléchargement contient les onze textes JSON, le schéma exporté, le validateur et sept tests. Certains fichiers sont volontairement invalides : ils servent aux essais de rejet. Le script écrit un rapport avec le nom du cas, le résultat attendu, l’acceptation obtenue et le type d’erreur.

Afficher le script Python complet
"""Réponses IA simulées : syntaxe, schéma strict et cohérence métier."""
from pathlib import Path
from datetime import date
from typing import Literal, Self
import copy
import json
import re
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator


class Ligne(BaseModel):
    model_config = ConfigDict(strict=True, extra="forbid")
    article: str = Field(min_length=1)
    quantite: int = Field(gt=0)
    prix_centimes: int = Field(ge=0)


class Commande(BaseModel):
    model_config = ConfigDict(strict=True, extra="forbid")
    devise: Literal["EUR"]
    date_commande: str
    date_livraison: str
    lignes: list[Ligne] = Field(min_length=1)
    total_centimes: int = Field(ge=0)

    @field_validator("date_commande", "date_livraison")
    @classmethod
    def date_iso(cls, valeur):
        if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", valeur):
            raise ValueError("Format attendu : AAAA-MM-JJ.")
        date.fromisoformat(valeur)
        return valeur

    @model_validator(mode="after")
    def coherence(self) -> Self:
        if self.date_livraison < self.date_commande:
            raise ValueError("La livraison précède la commande.")
        total = sum(l.quantite * l.prix_centimes for l in self.lignes)
        if total != self.total_centimes:
            raise ValueError("Total différent de la somme des lignes.")
        return self


def objet_sans_doublon(paires):
    objet = {}
    for cle, valeur in paires:
        if cle in objet:
            raise ValueError("Clé JSON répétée.")
        objet[cle] = valeur
    return objet


def constante_invalide(valeur):
    raise ValueError("Constante non JSON : " + valeur)


def valider(texte):
    objet = json.loads(texte, object_pairs_hook=objet_sans_doublon,
                       parse_constant=constante_invalide)
    return Commande.model_validate(objet)


def cas():
    valide = {"devise": "EUR", "date_commande": "2026-10-01", "date_livraison": "2026-10-05",
              "lignes": [{"article": "cahier", "quantite": 2, "prix_centimes": 350}],
              "total_centimes": 700}
    exemples = {"valide": (json.dumps(valide), True)}
    mutations = {"total_faux": lambda x: x.update(total_centimes=701),
                 "entier_en_texte": lambda x: x["lignes"][0].update(quantite="2"),
                 "booleen": lambda x: x["lignes"][0].update(quantite=True),
                 "champ_inattendu": lambda x: x.update(confiance=0.99),
                 "chronologie": lambda x: x.update(date_livraison="2026-09-30"),
                 "date_impossible": lambda x: x.update(date_livraison="2026-02-30"),
                 "liste_vide": lambda x: x.update(lignes=[])}
    for nom, modifier in mutations.items():
        objet = copy.deepcopy(valide)
        modifier(objet)
        exemples[nom] = (json.dumps(objet), False)
    exemples["cle_repetee"] = (json.dumps(valide)[:-1] + ', "total_centimes": 700}', False)
    exemples["syntaxe_invalide"] = ('{"devise": "EUR",}', False)
    # Un article inventé reste cohérent : la validation locale ne prouve pas la vérité.
    invente = copy.deepcopy(valide)
    invente["lignes"][0]["article"] = "produit-inexistant-dans-le-catalogue"
    exemples["coherent_mais_non_verifie"] = (json.dumps(invente), True)
    return exemples


def main():
    dossier = Path(__file__).resolve().parent
    fixtures = dossier / "exemples-json"
    fixtures.mkdir(exist_ok=True)
    resultats = []
    for nom, (texte, attendu) in cas().items():
        (fixtures / (nom + ".json")).write_text(texte + "\n", encoding="utf-8")
        try:
            valider(texte)
            accepte, erreur = True, None
        except ValueError as exc:
            accepte, erreur = False, type(exc).__name__
        resultats.append({"cas": nom, "accepte": accepte, "attendu": attendu, "erreur": erreur})
    texte = json.dumps(resultats, ensure_ascii=False, indent=2)
    (dossier / "resultats.json").write_text(texte + "\n", encoding="utf-8")
    (dossier / "schema.json").write_text(json.dumps(Commande.model_json_schema(), indent=2) + "\n", encoding="utf-8")
    print(texte)


if __name__ == "__main__":
    main()

Le fichier schema.json décrit les contraintes exportables du modèle. Il ne remplace pas l’exécution de la règle de somme ou de chronologie : toutes les contraintes Python personnalisées ne deviennent pas automatiquement des contraintes exécutées par un fournisseur de génération structurée.

Où placer ce contrôle dans une application ?

Conservez le texte reçu, validez-le, puis ne transmettez à l’étape suivante que l’objet accepté. En cas de rejet, produisez un diagnostic adapté au champ concerné ou demandez une nouvelle réponse selon un nombre d’essais limité. Ne transformez pas automatiquement un échec en succès en ajoutant des valeurs par défaut qui n’ont aucune source.

Après la validation locale, vérifiez les informations externes nécessaires : existence d’un article dans un catalogue, prix applicable, référence de commande autorisée ou date provenant du document original. Ce sont des vérifications séparées. Le script proposé ne les simule pas comme si elles avaient eu lieu.

Un nom de champ tel que confiance ne constitue pas une mesure de fiabilité. Dans notre contrat, ce champ supplémentaire est simplement rejeté. Si votre application doit l’accepter, définissez d’abord sa signification et son mode de validation ; une valeur fournie par le modèle ne certifie pas la vérité du contenu.

Enfin, ce validateur suppose de petits objets pédagogiques. Une application réelle doit aussi définir la taille maximale des réponses et le nombre de lignes autorisées selon son usage. Les règles métier présentées ici ne constituent pas un système complet de traitement de commandes.

Une sortie structurée est un point de départ

Vous pouvez reproduire ces contrôles avec une réponse provenant de n’importe quel générateur, puisqu’aucun SDK fournisseur n’est nécessaire. Pour comprendre le contexte, consultez le guide des grands modèles de langage. Si vous envisagez une exécution locale, le calcul de mémoire Qwen3 traite une autre contrainte, celle des ressources matérielles.

Les scripts et exercices sont réunis sur la page de ressources Python gratuites. Pour cet exercice, retenez la séparation des preuves : lire le JSON, valider son contrat et vérifier ses faits demandent des contrôles différents.

Exemple original exécuté le 21 octobre 2026 avec Python 3.12.14. pydantic 2.12.5. Les versions testées figurent dans le téléchargement. Miniature illustrative générée avec image_gen ; les résultats présentés proviennent du code exécuté. Notre méthode éditoriale.

About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. Découvrez comment les données de vos commentaires sont traitées.