ITSkillsCenter
Intelligence Artificielle

Sorties structurées JSON fiables avec l’API OpenAI

13 دقائق للقراءة

Demander à un modèle « réponds-moi en JSON » fonctionne à peu près… jusqu’au jour où il ajoute une phrase d’introduction, oublie une virgule, ou renomme un champ. Votre code de traitement plante alors sur un JSONDecodeError, en production, devant un client. Les sorties structurées (Structured Outputs) suppriment ce risque : vous fournissez un schéma, et le modèle est contraint, au niveau du décodage, de produire un JSON qui le respecte exactement — tous les champs requis présents, tous les types corrects, aucune valeur hors de l’énumération autorisée.

Dans notre assistant de support client, cette fiabilité devient cruciale dès qu’on veut traiter les messages automatiquement plutôt que seulement y répondre. Ici, nous transformons chaque message entrant en une fiche structurée — catégorie, priorité, numéro de commande, résumé — directement exploitable par le reste du système : tri, routage, tableau de bord.

Guide principal de la série : Développer avec l’API OpenAI : modèles GPT, Responses API et bonnes pratiques.

🎯 Ce que vous allez apprendre

  • Comprendre pourquoi le mode JSON « libre » échoue et ce que garantit le mode strict.
  • Définir un schéma avec Pydantic et le passer au modèle via l’aide parse.
  • Récupérer un objet Python déjà validé, sans parsing manuel.
  • Écrire le même schéma à la main en JSON Schema quand vous n’utilisez pas Pydantic.
  • Gérer les champs optionnels, les énumérations et les refus du modèle.

🛠️ Ce que vous allez construire

Une fonction analyser_message qui prend le texte brut d’un client (« Bonjour, ma commande 4827 n’est toujours pas arrivée, c’est urgent ! ») et renvoie un objet Python typé : catégorie = livraison, priorité = haute, numéro = 4827, résumé en une phrase. Le résultat est garanti conforme : votre code en aval peut s’y fier sans vérification défensive.

Prérequis

  • Le premier appel à l’API OpenAI en place (clé, SDK).
  • Installer Pydantic : pip install pydantic.
  • Savoir lire une classe Python et des annotations de type (nom: str).
  • ⏱️ Temps estimé : environ 30 minutes.

Étape 1 — Comprendre le problème que cela résout

Sans contrainte, un modèle génère du texte mot à mot ; rien ne l’empêche d’entourer son JSON de commentaires ou de s’écarter du format à la moindre ambiguïté. On parlait autrefois de « prompt-and-pray » : on demande gentiment un format et on prie pour qu’il soit respecté. Cela tient en démonstration, pas en production, où une requête sur cent qui dévie suffit à casser une chaîne de traitement.

Les sorties structurées changent la nature de la garantie. Au lieu de suggérer un format dans la consigne, vous l’imposez au niveau du moteur de génération : le modèle ne peut littéralement pas produire un jeton qui violerait le schéma. Le résultat est toujours un JSON valide et conforme. Cette capacité est disponible sur les modèles récents — à partir de la génération GPT-4o et sur la gamme GPT-5 actuelle — ce qui couvre les modèles que vous utiliserez de toute façon.

Étape 2 — Définir le schéma avec Pydantic

La voie la plus confortable en Python consiste à décrire la forme attendue avec une classe Pydantic. C’est lisible, réutilisable, et le SDK sait la convertir tout seul en schéma pour le modèle. On modélise notre fiche d’analyse de message.

from pydantic import BaseModel
from enum import Enum

class Categorie(str, Enum):
    livraison = "livraison"
    paiement = "paiement"
    produit = "produit"
    autre = "autre"

class Analyse(BaseModel):
    categorie: Categorie
    priorite: str            # "haute", "moyenne" ou "basse"
    numero_commande: str | None
    resume: str

Chaque attribut devient un champ du JSON attendu. L’énumération Categorie restreint les valeurs possibles à une liste fermée : le modèle ne pourra pas inventer une catégorie « réclamation diverse » hors de la liste. Le type str | None sur numero_commande autorise l’absence de numéro quand le client n’en mentionne aucun. Cette classe est à la fois la spécification envoyée au modèle et le type que vous manipulerez ensuite : une seule source de vérité.

Étape 3 — Appeler le modèle avec l’aide parse

Le SDK propose une méthode parse qui prend directement la classe Pydantic, transmet le schéma au modèle, puis désérialise la réponse en une instance de cette classe. Vous n’écrivez aucun code de parsing : vous récupérez un objet Python prêt à l’emploi.

from openai import OpenAI

client = OpenAI()

def analyser_message(texte):
    reponse = client.responses.parse(
        model="gpt-5.4",
        input=[
            {"role": "system", "content":
                "Tu analyses des messages de clients d'une boutique. "
                "Extrais la categorie, la priorite, le numero de commande "
                "s'il est cite, et un resume en une phrase."},
            {"role": "user", "content": texte},
        ],
        text_format=Analyse,
    )
    return reponse.output_parsed

fiche = analyser_message(
    "Bonjour, ma commande 4827 n'est toujours pas arrivee, c'est urgent !"
)
print(fiche.categorie, "/", fiche.priorite, "/", fiche.numero_commande)
print(fiche.resume)

Le paramètre text_format reçoit la classe ; reponse.output_parsed renvoie une instance d’Analyse. On accède aux champs avec la notation pointée, comme sur n’importe quel objet — et l’éditeur de code propose même l’autocomplétion, puisque le type est connu. Lancez le script : vous obtenez categorie=livraison, priorite=haute, numero_commande=4827 et un résumé propre. Aucune désérialisation manuelle, aucune vérification de format : le contrat est tenu par construction.

Point d’étapefiche est bien un objet typé dont les champs correspondent à votre classe. Testez un message sans numéro de commande : numero_commande doit valoir None et non une chaîne vide ou un numéro inventé.

Étape 4 — Écrire le schéma à la main en JSON Schema

Si vous n’utilisez pas Pydantic — par exemple dans un autre langage, ou pour un schéma généré dynamiquement — vous fournissez le JSON Schema directement via le paramètre text de l’API Responses. C’est plus verbeux mais c’est exactement ce que Pydantic produit en coulisses.

schema = {
    "type": "object",
    "properties": {
        "categorie": {"type": "string",
                       "enum": ["livraison", "paiement", "produit", "autre"]},
        "priorite": {"type": "string", "enum": ["haute", "moyenne", "basse"]},
        "numero_commande": {"type": ["string", "null"]},
        "resume": {"type": "string"},
    },
    "required": ["categorie", "priorite", "numero_commande", "resume"],
    "additionalProperties": False,
}

reponse = client.responses.create(
    model="gpt-5.4",
    input=[{"role": "user", "content": texte}],
    text={"format": {"type": "json_schema", "name": "analyse",
                     "strict": True, "schema": schema}},
)
import json
fiche = json.loads(reponse.output_text)

On enveloppe le schéma dans text.format avec "type": "json_schema" et "strict": True. La réponse arrive cette fois comme texte JSON dans output_text, que l’on désérialise avec json.loads — mais en toute sécurité, puisqu’on sait qu’il est conforme. Notez deux exigences du mode strict, visibles ici : additionalProperties doit valoir False, et tous les champs doivent figurer dans required. C’est ce point qui surprend le plus souvent.

Étape 5 — Champs optionnels, énumérations et refus

Le mode strict impose que chaque champ déclaré soit requis. Comment, alors, rendre une donnée « optionnelle » ? On ne la retire pas de required : on autorise la valeur null via un type union, exactement comme numero_commande plus haut (["string", "null"], ou str | None en Pydantic). Le champ est donc toujours présent, mais peut valoir null quand l’information manque. C’est plus rigoureux qu’un champ absent, car votre code n’a jamais à tester l’existence d’une clé.

Les énumérations sont votre meilleur allié pour fiabiliser un classement : en listant les valeurs autorisées, vous éliminez les variantes orthographiques (« Haute », « urgent », « URGENT ») et obtenez des étiquettes propres, directement filtrables. Enfin, un modèle peut, dans de rares cas, refuser de répondre — par exemple face à une demande inappropriée. Le SDK expose alors un champ de refus plutôt qu’une fiche : prévoyez de le tester avant d’utiliser output_parsed, et traitez le refus comme un cas métier (message neutre, escalade humaine) plutôt que comme une panne.

Point d’étape — Tous vos champs sont dans required et les champs facultatifs acceptent null. Si l’API renvoie une erreur de schéma, c’est presque toujours qu’un champ manque dans required ou que additionalProperties n’est pas à False.

Quand préférer les sorties structurées

Toutes les réponses n’ont pas vocation à être structurées. Pour une réponse conversationnelle adressée à un humain, le texte libre reste le bon format. Les sorties structurées brillent dès que la réponse doit être consommée par du code : extraction d’entités, classification, remplissage de formulaire, étape intermédiaire d’un agent, alimentation d’une base. La règle pratique : si la sortie part vers un humain, laissez du texte ; si elle part vers une fonction, un if ou une colonne de table, imposez un schéma. Cette discipline supprime une catégorie entière de bugs et rend vos traitements déterministes là où ils doivent l’être.

Décortiquer une analyse, champ par champ

Reprenons le message « Bonjour, ma commande 4827 n’est toujours pas arrivée, c’est urgent ! » et suivons le raisonnement que le schéma impose au modèle. Pour le champ categorie, le modèle confronte le message aux quatre étiquettes autorisées ; « pas arrivée » renvoie sans ambiguïté à livraison, et l’énumération l’empêche de choisir une formulation maison. Pour priorite, le mot « urgent » et le point d’exclamation orientent vers haute — un jugement que la consigne système peut affiner en précisant ce qui constitue une urgence pour votre activité. Le champ numero_commande est rempli par extraction directe : « 4827 » est repéré dans le texte. Et resume condense le tout en une phrase neutre, utile pour un coup d’œil rapide dans un tableau de bord.

Ce qui change tout par rapport à une réponse libre, c’est la stabilité du résultat sur des milliers de messages. Quelle que soit la tournure du client — poli, agacé, télégraphique — la sortie a toujours la même forme. Vous pouvez donc empiler ces fiches dans une base, compter les tickets par catégorie, déclencher une alerte quand la priorité haute dépasse un seuil, sans jamais écrire de code défensif pour rattraper un format qui aurait dérapé. La structure n’est plus une espérance, c’est une propriété du système.

Concevoir un bon schéma

La fiabilité technique étant acquise, la vraie difficulté se déplace vers la conception du schéma. Trois principes guident un schéma robuste. D’abord, préférer les énumérations aux champs libres partout où l’ensemble des valeurs est connu : une catégorie, une priorité, un statut gagnent à être fermés, car ils alimentent ensuite des filtres et des comptages. Ensuite, nommer les champs sans ambiguïté et documenter leur intention dans la consigne : un champ resume peut signifier « résumé du problème » ou « résumé de la conversation » — le modèle remplit mieux ce qu’il comprend clairement. Enfin, garder le schéma aussi plat et minimal que possible : chaque champ superflu est une occasion d’erreur et un coût en jetons, tandis qu’une imbrication profonde complique autant la génération que l’exploitation.

Un dernier réflexe paie sur la durée : versionner le schéma. Quand votre fiche d’analyse évolue — un nouveau champ sentiment, une catégorie supplémentaire — traitez-la comme une migration de base de données, en pensant aux fiches déjà stockées dans l’ancien format. Cette hygiène vous évite de mélanger deux générations d’analyses incompatibles dans le même tableau de bord, et rend l’évolution de l’assistant prévisible plutôt que subie.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
Erreur « schema must have required … » Un champ déclaré n’est pas listé dans required Mettre tous les champs dans required ; rendre optionnel via type null
Erreur sur additionalProperties Clé absente ou à True Ajouter "additionalProperties": False sur chaque objet
Valeurs de catégorie incohérentes Champ libre au lieu d’une énumération Déclarer un enum avec la liste fermée des valeurs
output_parsed vaut None Le modèle a refusé de répondre Tester le refus avant d’utiliser le résultat
Modèle ancien sans support Modèle antérieur à GPT-4o Utiliser un modèle récent (gamme GPT-5)

✅ Récapitulatif

Vous transformez désormais un message brut en données fiables : une classe Pydantic décrit la forme, responses.parse renvoie un objet validé, et le mode strict garantit la conformité au niveau du moteur. Vous savez aussi écrire le schéma à la main, rendre un champ optionnel sans le retirer de required, contraindre un classement par énumération et gérer un refus. Cette brique alimente naturellement le tableau de bord de tri et le routage automatique de votre assistant.

🧾 Aide-mémoire

Élément Rôle
class X(BaseModel) Décrire la forme attendue
responses.parse(text_format=X) Appeler en exigeant ce format
response.output_parsed Récupérer l’objet validé
text={"format":{"type":"json_schema", ...}} Schéma manuel (sans Pydantic)
strict: True + tout en required Garantie de conformité
type ["string","null"] Champ optionnel correct

💪 À vous de jouer

Ajoutez à la classe Analyse un champ sentiment contraint à l’énumération « positif / neutre / négatif », et un champ produits_cites qui est une liste de chaînes. Relancez l’analyse sur trois messages variés et vérifiez la cohérence.

Voir une piste
class Sentiment(str, Enum):
    positif = "positif"
    neutre = "neutre"
    negatif = "negatif"

class Analyse(BaseModel):
    categorie: Categorie
    priorite: str
    numero_commande: str | None
    sentiment: Sentiment
    produits_cites: list[str]
    resume: str

Tutoriels associés

FAQ

Quelle différence avec le simple « mode JSON » ?
Le mode JSON garantit seulement que la sortie est un JSON syntaxiquement valide, pas qu’elle respecte votre schéma. Les sorties structurées garantissent les deux : validité et conformité au schéma fourni.

Dois-je quand même décrire le format dans la consigne ?
Le schéma impose la structure, mais une consigne claire aide le modèle à bien remplir les champs (sens de chaque champ, règles de priorité). Les deux sont complémentaires.

Pydantic est-il obligatoire ?
Non. C’est le chemin le plus confortable en Python, mais vous pouvez fournir le JSON Schema à la main, ce qui fonctionne dans tous les langages.

Le mode strict ralentit-il les réponses ?
La première requête avec un nouveau schéma peut subir un léger surcoût de préparation, ensuite l’impact est négligeable au regard de la fiabilité gagnée.

مشاركة
Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité