12k
All articles

Schéma JSON pour les sorties structurées des LLM

Les structured outputs JSON Schema expliqués: comment OpenAI, Gemini et Claude imposent un schéma valide, avec conseils et pièges.

OpenReplay Team
OpenReplay Team
Schéma JSON pour les sorties structurées des LLM

Les sorties structurées contraignent la réponse d’un LLM à un schéma JSON que vous fournissez : le modèle renvoie ainsi du JSON exploitable par la machine, conforme à vos champs, types et enums, au lieu d’une prose libre qu’il vous faut analyser à la main.

Quiconque a déjà mis en production une fonctionnalité basée sur un LLM connaît l’alternative : une regex pour supprimer les balises markdown parasites, un try/catch autour de JSON.parse, et une boucle de retry qui se déclenche plus souvent qu’on ne le souhaiterait. Ça tient la route, jusqu’au matin où, discrètement, ça ne tient plus.

Le fournisseur applique le schéma pendant la génération, ce qui transforme « espérons que le modèle renvoie du JSON valide » en un véritable contrat. Cela fonctionne aujourd’hui chez OpenAI, Google Gemini et Anthropic Claude. Tous trois acceptent le schéma JSON, donc le même schéma est portable et vous ne modifiez que le câblage de la requête, pas le contrat. Cet article couvre ce que sont les sorties structurées, comment le schéma JSON les pilote, comment l’application du schéma fonctionne en interne, comment la configurer pour chaque fournisseur, et les modes de défaillance contre lesquels il vaut mieux se prémunir.

Points clés à retenir

  • Le mode JSON garantit uniquement un JSON syntaxiquement valide ; les sorties structurées strictes garantissent un JSON conforme à votre schéma spécifique : c’est la différence entre « ça parse » et « ça contient les champs dont vous avez besoin ».
  • L’application du schéma repose sur le décodage contraint (constrained decoding) : à chaque token, le modèle ne peut émettre que des continuations qui maintiennent la sortie valide au regard de votre schéma. La conformité est donc imposée pendant la génération, et non vérifiée après coup.
  • OpenAI, Gemini et Claude parlent tous le schéma JSON, donc un même schéma est portable d’un fournisseur à l’autre ; Pydantic et Zod se compilent en schéma JSON, ce qui correspond au workflow réellement adopté par la plupart des équipes.
  • Même avec le mode strict activé, un refus ou une réponse tronquée pour dépassement de longueur renvoie un statut de succès sans pour autant produire un JSON conforme au schéma : validez donc l’objet analysé avant de lui faire confiance.
  • Chaque fournisseur ne prend en charge qu’un sous-ensemble du schéma JSON : des mots-clés comme minimum, pattern ou la récursion profonde peuvent être ignorés ou rejetés. Vérifiez la documentation du sous-ensemble pris en charge par chaque fournisseur.

Du texte libre au JSON contraint par schéma

Laissés à eux-mêmes, les LLM produisent du texte libre qui casse les parseurs : ils ajoutent de la prose autour du JSON, omettent des guillemets ou inventent des champs. Les sorties structurées résolvent ce problème en contraignant la réponse à un schéma JSON, de sorte que la sortie soit exploitable par la machine et analysable de façon fiable.

C’est une évolution par rapport à l’ancien mode JSON. OpenAI a introduit le mode JSON en 2023 comme moyen d’imposer du JSON valide, mais il promet seulement que la sortie sera analysable, pas qu’elle respectera un schéma que vous définissez. Les sorties structurées strictes combleront ce manque en imposant le schéma lui-même. Lors de la propre évaluation d’OpenAI sur le respect de schémas complexes, le modèle gpt-4o-2024-08-06 avec Structured Outputs a obtenu 100 %, contre moins de 40 % pour l’ancien gpt-4-0613. Il s’agit d’un benchmark propre à un modèle, pas d’une garantie universelle, mais il illustre bien le passage de « ça parse en général » à « c’est conforme au schéma ».

Quelle est la place du schéma JSON ?

Un schéma JSON est un contrat déclaratif pour vos données : il déclare les types, les champs obligatoires, les enums et les contraintes de valeurs. Vous le transmettez avec la requête, et le fournisseur contraint la génération pour qu’elle s’y conforme. Voici à quoi ressemble un schéma compact pour une extraction de contact :

{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "email": { "type": "string" },
    "plan_interest": {
      "type": "string",
      "enum": ["starter", "pro", "enterprise"]
    }
  },
  "required": ["name", "email", "plan_interest"],
  "additionalProperties": false
}

Peu de développeurs écrivent ces schémas à la main en production. Le workflow courant consiste à définir la structure avec Pydantic (Python) ou Zod (TypeScript) et à laisser le SDK générer le schéma JSON. Les SDK d’OpenAI prennent cela en charge directement : donnez-leur un objet Pydantic ou Zod et ils génèrent le schéma JSON correspondant, reconvertissent la réponse en votre objet typé et remontent les refus pour vous. Gemini a ajouté la même commodité, en étendant la prise en charge du schéma JSON à tous les modèles Gemini activement supportés, afin que les schémas Pydantic et Zod fonctionnent sans étape de conversion.

Si vous préférez éviter d’assembler à la main le schéma ou le wrapper propre à chaque fournisseur, le générateur de schémas JSON d’OpenReplay fait les deux directement dans le navigateur. Vous ajoutez des champs avec leurs types, descriptions, enums et imbrications, ou vous collez un exemple de JSON pour en déduire un point de départ, puis vous copiez le résultat depuis le panneau d’export, qui propose le schéma JSON, le response_format d’OpenAI, l’outil de type function d’OpenAI, Anthropic, Gemini, Zod et Pydantic. Rien de ce que vous saisissez ne quitte la page.

Comment fonctionne l’application du schéma ?

Les sorties structurées fonctionnent parce que le fournisseur contraint le décodage : à chaque étape de génération, le modèle ne peut produire que des tokens qui maintiennent la sortie valide au regard de votre schéma. Le schéma est compilé en une grammaire (ou une machine à états finis), et les tokens qui la violeraient sont masqués avant l’échantillonnage. La documentation d’Anthropic décrit le même mécanisme : votre schéma est compilé en une grammaire, et l’échantillonnage contraint maintient la génération à l’intérieur de celle-ci, si bien que le modèle n’a aucun moyen d’émettre un token qui casserait le schéma.

Le même principe se généralise au-delà du JSON. Pour les modèles locaux et auto-hébergés, llama.cpp utilise des fichiers de grammaire GBNF et Outlines applique des contraintes basées sur des regex et des grammaires : tous deux imposent des formats arbitraires (SQL, DSL personnalisés ou JSON) selon le même principe de masquage de tokens.

Les sorties structurées selon les fournisseurs

Les trois principaux fournisseurs parlent tous le schéma JSON, donc le modèle de conception est portable. Ce qui diffère, c’est le câblage de la requête et les signaux d’échec.

FournisseurOù placer le schémaDialecte de schémaSignal de refus / d’incomplétude
OpenAIresponse_format (Chat Completions) ou text.format (Responses API), avec strict: trueSous-ensemble du schéma JSONchamp refusal ; finish_reason: "length"
GeminiresponseFormat.text (mimeType + schema) dans generationConfigSous-ensemble du schéma JSON (incl. anyOf, $ref)candidat tronqué ; rejet des schémas trop complexes
Claudeoutput_config.format, ou strict: true sur l’input_schema d’un outilSous-ensemble du schéma JSONstop_reason: "refusal" / "max_tokens"

OpenAI. Définissez strict: true et transmettez le schéma. Le guide d’OpenAI recommande de démarrer les nouveaux projets sur ses modèles actuels et précise que l’API Responses a déplacé le paramètre : utilisez response_format: { type: "json_schema", strict: true } avec Chat Completions, ou text: { format: { type: "json_schema", strict: true } } avec l’API Responses.

Gemini. Fournissez le schéma via generationConfig :

response = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="Extract the contact from this email...",
    config={
        "response_format": {
            "text": {
                "mime_type": "application/json",
                "schema": person_schema,
            }
        },
    },
)

Claude. Anthropic propose des sorties structurées natives, il ne s’agit plus d’un contournement par appel d’outil. Il existe deux fonctionnalités complémentaires : les sorties JSON via output_config.format pour le corps de la réponse, et l’utilisation stricte d’outils via strict: true pour les entrées d’outils, utilisables indépendamment ou conjointement. L’utilisation stricte d’outils garantit que les arguments d’un appel correspondent à son input_schema, car ce schéma est compilé en une grammaire qui contraint l’échantillonnage : la même famille de techniques que celles employées par OpenAI et Gemini. Notez que la surface de l’API a changé lors du passage en disponibilité générale : le paramètre output_format est devenu output_config.format, et les en-têtes bêta ne sont plus requis.

Pièges et bonnes pratiques

Le mode strict ne garantit pas une sortie analysable, et la prise en charge des schémas n’est pas universelle. Prémunissez-vous contre les points suivants.

Gardez des schémas plats. Les structures profondément imbriquées ou récursives sont la cause la plus fréquente des erreurs « schéma trop complexe » et d’une dégradation du raisonnement. Claude le signale directement en renvoyant une erreur 400 lorsque la grammaire compilée devient trop volumineuse, et la documentation de Gemini avertit que les schémas très volumineux ou très profondément imbriqués peuvent être rejetés. Les noms de propriétés longs, les grands tableaux, les enums comportant de nombreuses valeurs et les objets remplis de propriétés optionnelles alourdissent tous le coût. Découpez les extractions volumineuses en schémas plus petits et plus plats.

Gérez les refus et les troncatures. Même avec le mode strict activé, un refus ou une réponse tronquée pour dépassement de longueur renvoie un statut de succès sans être pour autant un JSON conforme au schéma. OpenAI a ajouté un signal dédié pour cela : un champ refusal dans la réponse vous indique que le modèle a décliné la demande plutôt que de renvoyer quelque chose de conforme à votre schéma. Il n’existe pas de limite universelle de tokens en sortie. Les plafonds varient selon les modèles, et toute réponse qui atteint le plafond en plein milieu d’un objet produit du JSON invalide : dimensionnez donc votre max_tokens pour le pire des cas et vérifiez le plafond documenté de votre modèle.

Vérifiez le sous-ensemble pris en charge. Chaque fournisseur ne prend en charge qu’une partie du schéma JSON en mode strict. Le guide d’OpenAI indique explicitement qu’une bonne partie de la spécification est couverte, mais que certains éléments sont laissés de côté, pour des raisons de performance ou techniques. Des mots-clés comme minimum, pattern ou les valeurs par défaut peuvent être ignorés ou rejetés : consultez la documentation du sous-ensemble pris en charge par le fournisseur plutôt que de présumer la prise en charge de la spécification complète.

Validez malgré tout. Comme les refus et les troncatures produisent des réponses au statut valide mais au JSON invalide, analysez et validez l’objet au regard de votre schéma avant de lui faire confiance, même avec strict: true.

Raisonner d’abord, émettre ensuite. Contraindre la sortie peut réduire la qualité du raisonnement sur certaines tâches. Un guide pratique consacré aux sorties structurées de Claude traite cela comme un véritable compromis face au raisonnement étendu : si une tâche gagne davantage à bénéficier du raisonnement du modèle qu’à garantir la conformité au schéma, laissez la phase de réflexion sans contrainte. Une voie médiane pragmatique consiste à laisser le modèle raisonner dans une phase de réflexion, puis à ne contraindre que le JSON final.

Les sorties structurées transforment les réponses des LLM en quelque chose que vous pouvez traiter comme une API typée, et ce modèle de conception se transpose sans heurt entre OpenAI, Gemini et Claude, puisque tous acceptent le schéma JSON. Commencez par définir votre structure avec Pydantic ou Zod, activez le mode strict chez votre fournisseur, gardez le schéma plat, et enveloppez l’analyse dans une validation qui gère les refus et les troncatures, puis branchez le même schéma sur le fournisseur que vous déployez.

FAQ

Quelle est la différence entre le mode JSON et les sorties structurées strictes ?

Le mode JSON garantit uniquement que le modèle renvoie du JSON syntaxiquement valide, analysable sans erreur, mais il ne garantit pas que la sortie soit conforme à un schéma particulier. Les sorties structurées strictes imposent votre schéma JSON spécifique pendant la génération, de sorte que l'objet renvoyé comporte les champs, types et enums que vous avez définis. La distinction est « ça parse » contre « ça contient les champs dont vous avez besoin ». OpenAI a introduit le mode JSON en 2023, puis a ajouté des sorties structurées contraintes par schéma pour combler ce manque.

Activer le mode strict garantit-il d'obtenir toujours du JSON valide et analysable ?

Non. Le mode strict contraint la génération de tokens à votre schéma lors d'une complétion normale, mais un refus pour raison de sécurité ou une réponse tronquée pour dépassement de longueur renvoie tout de même un statut de succès tout en produisant une sortie qui n'est pas un JSON conforme au schéma. OpenAI expose un champ refusal dédié et un finish_reason valant length ; Claude signale ces cas avec un stop_reason valant refusal ou max_tokens. Comme ces réponses renvoient un 200 et sont facturées, vous devriez analyser et valider l'objet au regard de votre schéma avant de lui faire confiance.

Puis-je réutiliser le même schéma JSON avec OpenAI, Gemini et Claude ?

Dans une large mesure, oui. OpenAI, Google Gemini et Anthropic Claude acceptent tous le schéma JSON, donc le même schéma est portable et vous ne modifiez que le câblage de la requête, pas le contrat. Ce qui diffère, c'est l'endroit où placer le schéma : OpenAI utilise response_format ou text.format avec strict à true, Gemini imbrique le schéma sous responseFormat.text dans generationConfig, et Claude utilise output_config.format ou l'utilisation stricte d'outils. Chaque fournisseur ne prend en charge qu'un sous-ensemble du schéma JSON : vérifiez donc les mots-clés non pris en charge dans la documentation du sous-ensemble de chaque fournisseur avant de présumer une portabilité totale.

Pourquoi mon schéma est-il rejeté comme trop complexe, et comment y remédier ?

Les limites de complexité proviennent de structures profondément imbriquées ou récursives, de noms de propriétés longs, de limites de tableaux élevées, d'enums comportant de nombreuses valeurs, ou d'objets avec de nombreuses propriétés optionnelles. Claude renvoie une erreur 400 lorsque la grammaire compilée devient trop volumineuse, et Gemini peut rejeter des schémas très volumineux ou très profondément imbriqués. La solution consiste à garder les schémas plats et à découper les extractions volumineuses en schémas plus petits et plus plats. Les structures profondément imbriquées sont également une cause fréquente de dégradation du raisonnement : aplatir améliore donc à la fois l'acceptation et la qualité de la sortie.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.