JSON mode et schema enforcement
Mis à jour le 28 juillet 2026
JSON mode et schema enforcement
Le Structured Output d’OpenAI garantit que la réponse du modèle est un JSON valide conforme à un schéma que vous définissez. Fini le parsing fragile avec des regex ou les erreurs de format — le modèle est contraint au niveau de la génération elle-même.
La nuance est importante et elle change tout pour un développeur. Demander « réponds en JSON » dans le prompt est une prière : le modèle obéit la plupart du temps, puis un jour il préfixe sa réponse par « Voici le JSON demandé : », ajoute des backticks Markdown ou oublie une virgule, et votre json.loads lève une exception en production, à trois heures du matin, sur le lot le plus important de la semaine. Avec le Structured Output, la contrainte s’applique pendant le décodage : les tokens qui produiraient un JSON invalide ou non conforme ne peuvent tout simplement pas être choisis. Vous ne suppliez plus, vous imposez.
JSON mode vs Structured Output
OpenAI propose deux niveaux de contrainte, et la distinction se joue sur un seul mot. Le JSON mode garantit un JSON valide, sans aucune contrainte de structure ; le Structured Output garantit un JSON conforme à un JSON Schema que vous avez écrit.
Le JSON mode résout donc la moitié du problème. Vous êtes sûr de pouvoir parser la réponse, mais pas de savoir ce qu’elle contient : le modèle peut nommer un champ annee sur une requête et year sur la suivante, ou emballer sa liste dans un objet resultats qu’il n’avait pas utilisé la veille. Pour un prototype ou un affichage brut destiné à un humain, cela suffit ; dès que la sortie alimente du code qui accède aux champs par leur nom, il faut le second niveau.
from openai import OpenAI
client = OpenAI()
# JSON mode simple (JSON valide, structure libre)
response = client.responses.create(
model="gpt-5.6-terra",
instructions="Réponds toujours en JSON.",
input="Liste 3 langages de programmation avec leur année de création.",
text={"format": {"type": "json_object"}}
)
import json
data = json.loads(response.output_text)
print(data)
Structured Output avec JSON Schema
C’est la fonctionnalité la plus puissante. Vous définissez un schéma JSON, et l’API garantit que la sortie le respecte. L’exemple ci-dessous analyse le sentiment d’un avis client, et il illustre trois usages complémentaires du schéma.
L’enum sur sentiment interdit au modèle d’inventer une catégorie intermédiaire du type « plutôt positif » : il devra trancher entre les quatre valeurs prévues, ce qui rend la sortie directement utilisable dans un filtre ou un tableau de bord. La description du champ score sert de mini-prompt local, elle indique l’échelle attendue sans que vous ayez à l’expliquer dans les instructions générales, et elle reste attachée au champ même si le prompt évolue. Le tableau aspects, enfin, permet de décomposer un avis contradictoire — ici un produit apprécié mais une livraison ratée — au lieu d’écraser cette nuance dans une note unique qui ne rendrait service à personne.
response = client.responses.create(
model="gpt-5.6-terra",
input="Analyse le sentiment de : 'Le produit est excellent "
"mais la livraison était catastrophique.'",
text={
"format": {
"type": "json_schema",
"name": "sentiment_analysis",
"schema": {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positif", "negatif", "mixte", "neutre"]
},
"score": {
"type": "number",
"description": "Score de -1.0 (très négatif) à 1.0 (très positif)"
},
"aspects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"aspect": {"type": "string"},
"sentiment": {
"type": "string",
"enum": ["positif", "negatif", "neutre"]
}
},
"required": ["aspect", "sentiment"],
"additionalProperties": False
}
},
"resume": {"type": "string"}
},
"required": ["sentiment", "score", "aspects", "resume"],
"additionalProperties": False
},
"strict": True
}
}
)
result = json.loads(response.output_text)
# Garanti : result a exactement les champs définis
print(f"Sentiment : {result['sentiment']}")
print(f"Score : {result['score']}")
for aspect in result["aspects"]:
print(f" - {aspect['aspect']}: {aspect['sentiment']}")
Règles du Structured Output strict
Le mode strict: true impose ses propres contraintes sur le schéma. Tous les champs doivent figurer dans required, et additionalProperties doit valoir false sur chaque objet. Les types admis sont string, number, integer, boolean, array, object et null, auxquels s’ajoutent les enum pour contraindre les valeurs. En revanche, ni pattern ni minLength/maxLength ne sont acceptés : la validation fine se fait côté client.
Ces règles surprennent au premier essai, car elles interdisent le champ facultatif tel qu’on l’écrit habituellement en JSON Schema. La contrepartie est confortable : votre code n’a plus jamais à tester si une clé existe, elle est toujours là. Pour un champ qui peut légitimement être vide — un numéro de téléphone absent d’un document, une date de fin pour un contrat en cours — la réponse n’est pas de le retirer de required mais de l’autoriser à valoir null. Quant à l’absence de pattern et de bornes de longueur, elle annonce la leçon suivante : le schéma garantit la forme, la validation métier reste votre travail une fois la réponse reçue.
Helper pour construire les schémas
Écrire à la main le bloc text.format à chaque appel devient vite pénible et sujet aux fautes de frappe — une clé name oubliée, un strict mal placé, et l’erreur remonte de l’API sans indiquer où chercher. Un wrapper de quelques lignes centralise la structure, applique une température basse par défaut, puisque la variabilité n’apporte rien à une extraction, et rend le point d’appel lisible.
def structured_query(prompt: str, schema: dict,
schema_name: str = "response",
model: str = "gpt-5.6-terra",
system: str = "") -> dict:
"""Wrapper pour les requêtes Structured Output."""
response = client.responses.create(
model=model,
instructions=system or "Réponds en JSON selon le schéma fourni.",
input=prompt,
text={
"format": {
"type": "json_schema",
"name": schema_name,
"schema": schema,
"strict": True
}
},
temperature=0.1
)
return json.loads(response.output_text)
Exemple : extraction de données structurées
Voici le cas qui justifie à lui seul l’apprentissage de cette technique : transformer une facture reçue en texte brut en un objet exploitable par votre comptabilité. Le schéma décrit trois niveaux — l’en-tête de facture, un objet fournisseur imbriqué, et un tableau lignes dont chaque élément a la même forme. Le modèle doit donc à la fois repérer des informations dispersées dans le document et les ranger au bon endroit, y compris pour une ligne exprimée en heures plutôt qu’en unités, où le prix unitaire s’entend au taux horaire.
Notez que les totaux figurent dans le schéma comme des champs à extraire, pas à recalculer. On demande au modèle de lire ; l’arithmétique de contrôle se fait ensuite dans votre code, où une simple comparaison entre la somme des lignes et le total_ht extrait vous signalera les documents à faire vérifier par un humain.
invoice_schema = {
"type": "object",
"properties": {
"numero_facture": {"type": "string"},
"date": {"type": "string"},
"fournisseur": {
"type": "object",
"properties": {
"nom": {"type": "string"},
"siret": {"type": "string"},
"adresse": {"type": "string"}
},
"required": ["nom", "siret", "adresse"],
"additionalProperties": False
},
"lignes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantite": {"type": "integer"},
"prix_unitaire_ht": {"type": "number"},
"tva_pourcent": {"type": "number"}
},
"required": ["description", "quantite",
"prix_unitaire_ht", "tva_pourcent"],
"additionalProperties": False
}
},
"total_ht": {"type": "number"},
"total_ttc": {"type": "number"}
},
"required": ["numero_facture", "date", "fournisseur",
"lignes", "total_ht", "total_ttc"],
"additionalProperties": False
}
facture_text = """
Facture N° 2026-0042
Date : 15/03/2026
Fournisseur : TechServ SARL, SIRET 123 456 789 00012
12 rue de la Paix, 75002 Paris
- Développement API REST x1 : 3500.00 EUR HT (TVA 20%)
- Hébergement cloud (3 mois) x1 : 450.00 EUR HT (TVA 20%)
- Support technique x10h : 150.00 EUR HT/h (TVA 20%)
Total HT : 5450.00 EUR
Total TTC : 6540.00 EUR
"""
result = structured_query(
facture_text,
invoice_schema,
schema_name="invoice",
system="Extrais les données de cette facture."
)
Mesurer ce que la contrainte vous apporte
Reprenez ce principe sur un document que vous connaissez bien : définissez un schéma JSON pour extraire les informations clés d’un CV — nom, expériences, compétences, formation — et testez-le sur trois CV en texte brut, choisis délibérément hétérogènes dans leur mise en page. Vérifiez que la sortie respecte le schéma jusque dans le cas où une rubrique manque complètement.
Refaites ensuite la même extraction sans Structured Output, en demandant simplement du JSON dans le prompt, et conservez les six sorties côte à côte. C’est en comparant les deux séries que l’on mesure ce que la contrainte apporte : l’écart porte rarement sur le contenu extrait, presque toujours sur la stabilité des noms de champs et sur la présence des clés attendues.
Points clés à retenir
- Le Structured Output garantit un JSON conforme au schéma au niveau de la génération
- Utilisez
strict: truepour la conformité maximale - Tous les champs doivent être
requiredavecadditionalProperties: false - Idéal pour l’extraction de données, la classification, les pipelines de traitement
- La validation fine (longueur, pattern) se fait côté client après réception