Tutoriel

Qu’est-ce que l’AI Structured Output ? Comment JSON Schema rend le JSON des LLM fiable

« JSON uniquement » dans le prompt ne fait que baisser la probabilité. Structured Output fige la forme au décodage ; JSON Schema transforme champs, types et énumérations en contrat.

Brancher un modèle dans un pipeline, ce qui fait peur n’est pas une prose faible — c’est une valeur que le code ne peut pas avaler : une virgule manquante, un champ renommé, un nombre devenu chaîne. Structured Output existe pour cela : le modèle parle selon la forme déclarée pendant qu’il émet des tokens, au lieu d’écrire une prose que vous gratterez ensuite avec une regex. JSON Schema est la forme écrite la plus courante de cette forme. À la fin, vous saurez si vous bloquez sur la syntaxe, la forme ou la vérité métier — et qu’un schéma exige encore un contrôle local.

Pourquoi le JSON « par prompt » n’est pas fiable

« Renvoyez uniquement du JSON, sans explication » est le bricolage le plus courant. Parfois ça marche, parce que cela écrit une préférence dans le contexte. Ce n’est pas fiable, car une préférence n’est pas une contrainte : le modèle peut encore envelopper l’objet dans une clôture markdown, omettre une clé requise, ou transformer priority en un urgency plus « naturel ». Dès que l’aval exécute JSON.parse, l’échec n’est plus un problème de texte — toute la chaîne s’arrête.

L’échec plus discret : « ça parse, mais la forme est fausse ». Vous attendiez que items soit un tableau et vous recevez un objet ; un score numérique et vous recevez "0.91". Le code lit undefined ou concatène des chaînes, et le bug explose beaucoup plus loin. Un prompt n’arrête pas cette dérive : il ne refuse pas les tokens illégaux au décodage.

N’y voyez donc pas « un prompt plus sévère ». C’est une partie de la génération : le serveur compile le schéma en ensemble de tokens suivants autorisés, et le modèle ne peut pas émettre des parenthèses mal appariées ni une clé hors liste. Le prompt explique la tâche ; le schéma possède la forme.

Quelle couche Structured Output contraint vraiment

Pensez à trois couches. Mélangez-en deux et l’on croit que « le modèle fait n’importe quoi ». Première : la syntaxe — du JSON qui parse. Deuxième : la forme — noms, types, requis, énumérations conformes au schéma. Troisième : le sens — la classe est-elle la bonne, le montant est-il réel ? Structured Output couvre les deux premières. La troisième est toujours à vous.

Niveau Ce que vous configurez Ce que vous obtenez vraiment
Convention de prompt « JSON uniquement » Une préférence, pas un contrat
Mode JSON Type MIME ou json_object JSON probablement valide ; les noms de champs restent au modèle
Mode schéma JSON Schema plus un drapeau strict Forme, types, requis et énumérations suivent le schéma

OpenAI présente Structured Outputs comme l’étape après le JSON mode : les deux peuvent produire du JSON valide ; seul le premier garantit le schéma fourni. Comparaison officielle : Structured model outputs. Gemini trace la même ligne entre « juste du JSON » et « émettre les champs du schéma » ; pour les interrupteurs, voir le guide JSON de l’API Gemini — cet article ne reprend pas le détail du SDK.

Une frontière facile à rater : un refus de sécurité, une troncature ou un échec d’outil peuvent ne pas entrer dans votre objet de succès. Certaines API renvoient un refusal séparé ou un contenu vide. Le schéma contraint le passage qui « parle dans le format », pas « cet appel réussira ».

Comment JSON Schema devient un contrat

JSON Schema est un vocabulaire pour documents JSON : types, requis, énumérations, plages numériques, éléments de tableau. Le parcours standard : Understanding JSON Schema. Branché à un modèle, ce vocabulaire gagne un second métier : il n’est plus seulement un contrôle a posteriori — il rétrécit l’espace de recherche pendant la génération.

Pour l’ingénierie, le schéma est un contrat partagé entre compilation et exécution. Pydantic, Zod et les types générables Swift retombent souvent sur un JSON Schema indépendant du langage, à passer au modèle cloud, à journaliser, à rejouer en fixture. Une seule table de champs, pour que l’app ne dise pas totalCents, l’API amount, et le prompt « montant ».

Écrivez l’objet que vous lirez vraiment, pas un modèle du monde. Chaque champ optionnel de trop est une chance de le remplir faux ou de le laisser vide. Les requis vont dans required, les ensembles fermés dans enum, les bornes numériques dans minimum / maximum. Un description de propriété est souvent plus stable qu’une nouvelle explication dans le prompt, car la contrainte est liée au décodage.

Le mode strict ajoute souvent une règle : les objets doivent avoir additionalProperties: false, et chaque champ déclaré entre dans required. Pour une valeur vraiment optionnelle, n’« omettez pas required » — autorisez null. Sinon le modèle peut supprimer la clé alors que votre code suppose encore obj.field.

Comment chaque API attache un schéma

Les vendeurs disent tous Structured Output ; les champs d’emballage diffèrent. Posez d’abord deux questions : ce schéma contraint-il la réponse finale ou les arguments d’outil ? Ce snapshot de modèle gère-t-il vraiment le mode strict ? N’imaginez pas qu’un mot-clé de doc se comporte pareil sur chaque endpoint.

OpenAI : json_schema plus strict

Dans Chat Completions, mettez response_format à json_schema et activez strict: true. L’API Responses écrit la même chose sous un champ de format texte. Les règles de schéma sont identiques ; seul l’emballage change. La requête ci-dessous extrait un ticket : catégorie en énumération, compte nullable.

{
  "model": "gpt-4o-2024-08-06",
  "messages": [
    { "role": "system", "content": "Extract the ticket into the schema." },
    { "role": "user", "content": "Checkout 500s on a saved card. Account acct_8842." }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "support_ticket",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "summary": { "type": "string" },
          "category": {
            "type": "string",
            "enum": ["billing", "bug", "account", "other"]
          },
          "severity": { "type": "integer" },
          "account_id": {
            "anyOf": [{ "type": "string" }, { "type": "null" }]
          }
        },
        "required": ["summary", "category", "severity", "account_id"],
        "additionalProperties": false
      }
    }
  }
}

Traitez la réponse comme « tout est du JSON ». Ne grattez pas d’abord une clôture avec une regex. L’assistant parse du SDK peut désérialiser en objet typé ; gérez aussi la branche de refus — un trip de sécurité peut ne pas coller au schéma de succès.

Gemini et les autres piles

Gemini déclare « c’est du JSON » avec un type MIME, puis fige les champs avec responseSchema ou responseJsonSchema. Le second est plus proche du JSON Schema standard et convient à anyOf, $ref et aux plages numériques. Détails et exemples Python / JS : le tutoriel Gemini Structured Output.

Les modèles on-device d’Apple utilisent @Generable — une forme à la compilation, pas un fichier JSON Schema tapé à la main. Dès que vous quittez le processus, appelez HTTP ou écrivez un journal, il faut encore du JSON sérialisable. Comment se coupent les trois chaînes : Agents Apple et JSON. Quand un agent externe passe par MCP ou REST, la charge est presque toujours du JSON, et le schéma reste la table que l’adaptateur doit aligner.

Un schéma que l’on peut mettre en production

Le schéma ci-dessous modélise une classification de ticket : catégorie fermée, priorité entière, résumé chaîne. C’est ce qu’un schéma doit posséder — la forme, pas « ce ticket doit-il être urgent métier ».

{
  "type": "object",
  "properties": {
    "category": {
      "type": "string",
      "enum": ["billing", "bug", "feature"],
      "description": "Ticket category"
    },
    "priority": {
      "type": "integer",
      "minimum": 1,
      "maximum": 5
    },
    "summary": {
      "type": "string"
    }
  },
  "required": ["category", "priority", "summary"],
  "additionalProperties": false
}

Décrivez les éléments de tableau avec items. Pour une liste de longueur fixe, plutôt tuple, voyez prefixItems côté JSON Schema. Inlinez d’abord les objets imbriqués. N’employez $defs + $ref que lorsque la même structure apparaît une troisième fois, ou qu’un nœud d’arbre se pointe lui-même. Abstraire trop tôt rend les erreurs illisibles : si le serveur refuse le schéma, vous affrontez un gros objet déployé.

Ne décrivez pas la structure une fois dans le prompt et une fois dans le schéma. La double description fait osciller le modèle entre deux récits et gaspille des tokens d’entrée. La tâche reste dans le prompt ; noms et types seulement dans le schéma. Si les types locaux changent un champ, le schéma de la requête doit changer avec — sinon la prod voit naître ou manquer des clés en silence.

Après la contrainte, il faut encore valider en local

Structured Output calme beaucoup la couche d’analyse. Il ne garantit ni la bonne classe, ni qu’un nombre respecte votre plafond métier. Une énumération limite l’ensemble ; elle n’empêche pas un mauvais choix. Priorité 5 est valide ; « ça aurait dû être 2 » l’est aussi. Échantillonnez le chemin critique ou ajoutez des règles.

En production, traitez le retour du modèle comme du JSON ordinaire. Formatage JSON pour voir l’imbrication, Validation JSON pour confirmer le parse, puis jetez le même objet dans JSON Schema pour un second contrôle local. Face à un jeu d’or, utilisez JSON Diff pour voir d’un coup un décalage d’ordre de clés ou de champs nullables.

Gardez trois fixtures : ticket.valid.json, ticket.missing-field.json, ticket.wrong-enum.json. La première confirme le happy path ; les deux suivantes confirment que le contrôle local refuse vraiment. Les erreurs déjà contraintes côté modèle doivent rester reproductibles en local — sinon le jour où vous changez de modèle ou coupez le mode strict, le pipeline le découvre hors ligne.

FAQ

Quelle différence entre Structured Output et JSON mode ?

Le JSON mode garantit un JSON qui parse. Structured Output garantit en plus le JSON Schema fourni : noms, types, requis et énumérations suivent le contrat. Si le code lit des champs nommés, prenez le second.

Avec un schéma, faut-il encore écrire « JSON uniquement » ?

Une phrase courte suffit. Ne recolliez pas la table des champs dans le prompt. La forme appartient au schéma. Deux descriptions baissent la qualité et gaspillent le quota.

Comment représenter un champ optionnel en mode strict ?

La plupart des implémentations strictes ferment les propriétés supplémentaires et exigent tous les champs listés. Rendez les valeurs optionnelles nullables — une union chaîne-ou-null — au lieu de retirer la clé de required.

Le métier peut-il encore être faux si le schéma passe ?

Oui. Un schéma possède la forme, pas la vérité. Une mauvaise classe, un montant halluciné, un remplissage forcé au lieu d’un refus : tout cela peut être du JSON valide. La prod a encore besoin de règles, d’échantillons ou d’un regard humain.

Conclusion et suite

Structured Output n’est pas un tour de rédaction. Il transforme la forme JSON d’un vœu en contrainte de décodage. JSON Schema est la façon la plus courante d’écrire cette contrainte : requis, énumérations, plages et interdiction de clés en trop décident si l’aval peut JSON.parse de façon stable et lire les champs attendus. Les noms d’interrupteurs changent selon l’API ; les couches non — syntaxe, forme, sens. Ne les mélangez pas.

Ensuite, écrivez un petit schéma que vous lirez vraiment, faites passer un chemin d’extraction ou de classification en mode strict, puis validez le retour dans le navigateur. Ouvrez le guide précédent pour les exemples Gemini ; l’article Apple pour voir comment les arguments d’agent deviennent du JSON.