← Retour au blog

Guide complet d’OpenAI Structured Outputs : comment faire produire à GPT un JSON conforme à JSON Schema de manière stable ?

Guide complet d’OpenAI Structured Outputs : comment faire produire à GPT un JSON conforme à JSON Schema de manière stable ?

Ce guide s’adresse aux développeurs backend, aux ingénieurs data et aux concepteurs d’agents qui doivent transmettre des sorties GPT vers une base de données, une API ou un outil d’exécution. Il compare JSON mode et Structured Outputs, détaille la conception d’un schéma, la validation sémantique, la gestion des refus et des réponses tronquées, puis propose une méthode de régression adaptée aux pipelines de production.

Dans ses évaluations publiées, OpenAI indique que gpt-4o-2024-08-06 a atteint 100 % de conformité au schéma testé avec Structured Outputs, contre moins de 40 % pour gpt-4-0613 dans la même évaluation de suivi de schémas complexes. Ce résultat ne doit pas être généralisé à tous les modèles ni à toutes les données : pour obtenir une sortie GPT exploitable, il faut activer Structured Outputs avec strict: true, utiliser un sous-ensemble de JSON Schema compatible, puis ajouter les contrôles de refus, de troncature et de cohérence métier. (openai.com)

Cette approche convient aux développeurs backend qui doivent parser une réponse sans bricolage, aux ingénieurs data qui alimentent une base ou une file de messages, ainsi qu’aux concepteurs d’agents qui doivent séparer le format de la réponse finale des paramètres envoyés à un outil.

Pourquoi une simple consigne JSON ne suffit pas en production

Une instruction comme « répondez uniquement avec un objet JSON » peut améliorer la présentation, mais elle ne constitue pas un contrat de données. Même lorsque le document obtenu est syntaxiquement valide, plusieurs problèmes restent possibles :

  • un champ nécessaire est absent ou renommé ;
  • une valeur est produite sous forme de chaîne alors que le système attend un nombre ;
  • une propriété inattendue est ajoutée et ignorée silencieusement par le code aval ;
  • une catégorie libre varie entre « facturation », « billing » et « paiement » ;
  • un tableau contient des éléments de formes différentes ;
  • un agent reçoit des paramètres techniquement valides, mais non autorisés pour la ressource ciblée.

JSON mode répond surtout au besoin « obtenir un JSON valide ». La documentation de référence distingue cette ancienne approche de json_schema, recommandée lorsque le modèle utilisé prend en charge Structured Outputs. Le mode JSON ne force donc pas, à lui seul, l’adhérence à un schéma précis. (platform.openai.com)

Un deuxième coût caché apparaît dans les systèmes qui relancent automatiquement les requêtes. Sans distinction entre erreur de parsing, refus de sécurité, limite de longueur ou violation métier, le système dépense davantage de tokens tout en répétant parfois exactement la même erreur. La stratégie fiable consiste à classer l’échec avant de décider entre correction du schéma, réduction de l’entrée, intervention humaine ou nouvelle tentative.

Première étape : définir un contrat de données fermé, mais évolutif

Pour une extraction de facture, de fiche produit ou de notes de réunion, le schéma doit décrire ce que l’application accepte réellement, pas tout ce que le modèle pourrait éventuellement inventer. Un exemple minimal peut ressembler à ceci :

{
  "type": "object",
  "properties": {
    "customer_name": {
      "type": ["string", "null"],
      "description": "Nom du client lorsqu’il est explicitement identifiable"
    },
    "invoice_number": {
      "type": ["string", "null"],
      "description": "Identifiant de facture visible dans le document"
    },
    "total_amount": {
      "type": ["number", "null"],
      "description": "Montant total sans symbole monétaire"
    },
    "currency": {
      "type": ["string", "null"],
      "description": "Code de devise lorsqu’il est explicitement indiqué"
    }
  },
  "required": [
    "customer_name",
    "invoice_number",
    "total_amount",
    "currency"
  ],
  "additionalProperties": false
}

Le choix important n’est pas seulement le type de chaque propriété. Il concerne surtout la fermeture de l’objet et l’incertitude. Avec additionalProperties: false, l’application sait qu’une nouvelle clé n’est pas acceptable par accident. Avec des champs pouvant prendre null, le modèle dispose d’une réponse légale lorsque l’information manque. Cela vaut mieux que de le forcer à produire un identifiant ou un montant supposé.

La structure doit également être pensée pour l’évolution. Si une équipe ajoute ultérieurement tax_amount, elle doit décider si le changement mérite une nouvelle version de contrat, si les consommateurs peuvent tolérer l’absence de ce champ et si les anciennes données peuvent être relues. Un schéma « strict » mal versionné peut interrompre un pipeline parfaitement fonctionnel.

Besoin applicatif Décision de schéma recommandée Risque évité
Extraction d’un objet unique Propriétés explicites, champs obligatoires, additionalProperties: false Colonnes inattendues ou renommées
Extraction d’une liste Tableau avec items décrivant une forme unique Éléments hétérogènes et parsing conditionnel
Information absente Valeur nullable ou état explicite « inconnu » Valeur inventée pour satisfaire le contrat
Contrat amené à changer Version du schéma et migration documentée Régression silencieuse des consommateurs

OpenAI précise que Structured Outputs ne couvre qu’un sous-ensemble de JSON Schema lorsque le mode strict est utilisé. Il faut donc tester les constructions imbriquées, les unions, les références et les contraintes retenues avec le modèle et le SDK réellement déployés, au lieu de supposer qu’un schéma accepté par un validateur générique sera automatiquement accepté par l’API. (openai.com)

Deuxième étape : choisir les énumérations pour classer et router

Les scénarios de classification sont souvent plus fragiles que l’extraction de texte, car une sortie libre peut être « presque correcte » sans être routable. Pour diriger un ticket vers un service, un schéma plus sûr décrit une liste fermée :

{
  "type": "object",
  "properties": {
    "category": {
      "type": "string",
      "enum": [
        "facturation",
        "incident_technique",
        "compte",
        "autre",
        "revue_humaine"
      ],
      "description": "Catégorie opérationnelle la plus défendable"
    },
    "confidence_note": {
      "type": "string",
      "description": "Justification courte fondée sur le texte fourni"
    }
  },
  "required": ["category", "confidence_note"],
  "additionalProperties": false
}

L’énumération réduit la dérive lexicale, mais elle ne transforme pas le modèle en arbitre infaillible. Si aucun choix n’est défendable, autre ou revue_humaine doit être un résultat normal. Retirer ces issues pour augmenter artificiellement le taux de classification pousse le modèle vers une fausse précision, qui devient particulièrement coûteuse lorsqu’elle déclenche une action automatique.

La description de champ mérite également une formulation opérationnelle. « Catégorie du message » laisse trop de latitude. « Catégorie correspondant au motif principal explicitement présent dans le message » indique ce qui doit être observé. Il est préférable de décrire les frontières entre catégories dans les instructions plutôt que d’ajouter une longue taxonomie dans chaque propriété.

Pour un produit audio ou vidéo, la même logique s’applique à l’indexation : une piste peut être classée comme voix, musique, bruitage, silence ou revue_humaine. L’intérêt n’est pas d’obtenir une étiquette élégante, mais une valeur que le montage, la recherche ou le stockage peut réellement consommer.

Troisième étape : séparer réponse structurée et paramètres d’outil

Un agent comporte généralement deux contrats différents. Le premier décrit la réponse finale présentée à l’interface : résumé, statut, éléments à afficher. Le second décrit les arguments transmis à une fonction : identifiant de ressource, opération demandée, date ou quantité.

Dans le Function Calling, strict: true est placé dans la définition de la fonction afin que les arguments correspondent au schéma fourni. OpenAI distingue cette utilisation de la sortie structurée appliquée à une réponse qui ne déclenche pas d’outil. (help.openai.com)

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {
            "role": "user",
            "content": "Préparez l’export du projet vidéo nommé démo."
        }
    ],
    tools=[
        {
            "type": "function",
            "function": {
                "name": "prepare_export",
                "description": "Prépare un export sans le publier",
                "strict": True,
                "parameters": {
                    "type": "object",
                    "properties": {
                        "project_id": {
                            "type": "string",
                            "description": "Identifiant interne du projet"
                        },
                        "format": {
                            "type": "string",
                            "enum": ["wav", "mp3", "mov"]
                        }
                    },
                    "required": ["project_id", "format"],
                    "additionalProperties": False
                }
            }
        }
    ]
)

Même si project_id et format respectent le JSON Schema, le serveur d’application doit encore vérifier que le projet existe, que l’utilisateur possède le droit d’y accéder, que le format est autorisé pour ce projet et que l’export ne dépasse pas les limites opérationnelles. Un schéma valide ne doit jamais être confondu avec une autorisation d’exécuter.

La définition de l’outil doit donc être étroite. Une fonction execute_any_action avec de nombreux champs optionnels est plus difficile à sécuriser qu’une fonction limitée à une opération précise. Pour un agent, plusieurs fonctions ciblées rendent le routage plus explicite et facilitent les tests négatifs : ressource inexistante, rôle insuffisant, état déjà verrouillé ou format incompatible.

Quatrième étape : ajouter une validation sémantique avant l’écriture

La validation JSON Schema vérifie la forme déclarée. Elle ne sait pas, à elle seule, si une date correspond au calendrier métier, si un identifiant est présent dans la base ou si deux champs sont cohérents entre eux.

Couche de contrôle Exemple de question Action en cas d’échec
Syntaxe JSON Le document peut-il être décodé ? Rejeter et journaliser
JSON Schema Les champs, types et propriétés sont-ils conformes ? Mettre en quarantaine ou corriger le contrat
Sémantique La date, le montant ou l’identifiant est-il plausible ? Demander une vérification ou enrichir le contexte
Base de données La référence existe-t-elle et respecte-t-elle les contraintes ? Ne pas insérer
Autorisation L’opération est-elle permise pour cet utilisateur ? Bloquer l’action et tracer l’événement
Revue humaine Le cas est-il ambigu ou sensible ? Envoyer dans une file de contrôle

Un montant peut avoir le bon type number mais être exprimé dans la mauvaise devise. Une date peut être une chaîne conforme au motif attendu mais antérieure à la création du compte. Un identifiant peut être correctement formé et appartenir à un autre client. Ces contrôles doivent être appliqués avant l’insertion, et non après la publication dans une file ou l’exécution d’une action.

Pour les interfaces, cette distinction est également essentielle. Une UI générée peut respecter la structure attendue tout en affichant un bouton qui ne devrait pas être visible pour un rôle donné. La réponse structurée doit donc être considérée comme une proposition de données ou d’interface, soumise aux règles de l’application.

FAQ : traiter les échecs que le schéma ne résout pas

OpenAI comment garantit-il un JSON conforme à JSON Schema ?

Structured Outputs avec strict: true contraint la génération afin que la réponse suive le schéma fourni, dans la limite du sous-ensemble supporté. Cette garantie porte sur la structure : elle ne confirme ni la justesse factuelle des valeurs ni l’autorisation d’une action. L’application doit encore gérer les refus, les interruptions de génération et les contrôles métier.

Structured Outputs ou JSON mode : lequel choisir ?

JSON mode est adapté à un besoin de document JSON valide lorsque le contrat reste souple, mais il ne suffit pas pour imposer les propriétés et les types attendus. Lorsque le modèle et le point d’API le permettent, Structured Outputs avec un schéma strict est préférable pour une base de données, une file de messages ou un outil d’agent.

Pourquoi le parsing d’une sortie GPT échoue-t-il encore ?

Le parsing peut échouer parce que la génération a été interrompue, parce que la réponse est un refus ou parce que le schéma demandé n’est pas compatible avec la configuration utilisée. Il peut aussi réussir techniquement puis échouer lors de la conversion métier. Les journaux doivent donc conserver le statut de fin, le contenu brut et la version du contrat.

Comment traiter un refus avec Structured Outputs ?

Le refus doit être détecté avant l’appel au parseur métier. Il ne faut pas remplacer automatiquement le contenu manquant par une valeur par défaut, car cette valeur pourrait être interprétée comme une décision. Le système peut présenter une explication, demander une reformulation acceptable ou transférer le cas à une file de revue.

Une validation de JSON Schema suffit-elle après la réponse ?

Elle constitue seulement la première barrière. Les règles de cohérence entre champs, les contraintes SQL, l’existence des références et les droits d’accès doivent être vérifiés séparément. Pour les opérations financières, médicales, administratives ou destructives, une validation humaine peut être nécessaire même lorsque la structure est parfaite.

Cinquième étape : distinguer refus, troncature et erreur ordinaire

OpenAI indique qu’une réponse peut ne pas suivre le schéma si le modèle refuse la demande ou si la génération atteint une limite comme max_tokens avant d’être terminée. L’API expose alors des éléments permettant de différencier le refus et l’interruption prématurée, notamment le champ refusal et la raison de fin de génération. (openai.com)

Le traitement recommandé suit une logique de branchement :

  1. vérifier l’état général de la réponse ;
  2. rechercher un refus explicite ;
  3. examiner la raison de fin ou l’état d’incomplétude ;
  4. seulement ensuite parser le contenu comme JSON métier ;
  5. valider le schéma ;
  6. appliquer les règles sémantiques et d’autorisation.

Une troncature ne doit pas être corrigée en supprimant arbitrairement les derniers caractères. Le document original doit être conservé, avec l’entrée, le modèle, le schéma, les paramètres pertinents et l’erreur observée. Une nouvelle tentative peut être utile si l’entrée est raccourcie ou si la tâche est divisée, mais une relance identique ne résout pas une limite structurelle.

Un refus, de son côté, ne doit pas être envoyé dans la même file que les erreurs de réseau. Il a une signification fonctionnelle et peut exiger une réponse utilisateur différente. Cette séparation rend les tableaux de suivi plus fiables et évite de confondre indisponibilité technique et décision de sécurité.

Sixième étape : tester le sous-ensemble de schéma réellement utilisé

Le schéma doit être testé dans le même environnement que la production : même SDK, même méthode d’appel, même type de réponse et, lorsque c’est pertinent, même modèle. Les exemples à exécuter comprennent au minimum :

  • un objet simple avec tous les champs présents ;
  • un tableau vide et un tableau comportant plusieurs éléments ;
  • une information absente représentée par null ou par un état explicite ;
  • une entrée ambiguë nécessitant autre ou revue_humaine ;
  • une demande susceptible de provoquer un refus ;
  • une entrée longue pouvant provoquer une interruption ;
  • une tentative d’ajouter une propriété non déclarée ;
  • des valeurs valides en type mais invalides pour le métier.

OpenAI explique que le premier appel utilisant un nouveau schéma peut subir un délai de prétraitement, généralement inférieur à 10 secondes pour les schémas typiques, tandis que des schémas plus complexes peuvent demander jusqu’à une minute. Ces valeurs proviennent de l’annonce technique et ne doivent pas être transformées en engagement universel de latence ; elles justifient surtout le préchauffage et la réutilisation de contrats stables dans un pipeline. (openai.com)

Le parallélisme mérite aussi un test dédié. L’annonce Structured Outputs précise que la fonctionnalité n’est pas compatible avec les appels de fonctions parallèles dans certaines configurations et recommande de désactiver parallel_tool_calls lorsque l’adhérence stricte des arguments est nécessaire. (openai.com)

Construire une régression qui survit aux changements de schéma

Une suite de régression utile ne se limite pas à vérifier que le JSON est décodable. Chaque cas doit comporter une entrée, une version de schéma, un résultat attendu au niveau métier et une décision acceptable en cas d’ambiguïté.

Le jeu de tests peut être organisé en quatre familles :

  • normal : demandes fréquentes et documents bien formés ;
  • limite : champs absents, tableaux vides, textes courts ou très longs ;
  • malveillant : instruction contenue dans le document, tentative de modifier une opération ou de contourner une autorisation ;
  • migration : réponses produites avec une ancienne version et données relues après évolution du schéma.

La version du schéma doit être enregistrée avec chaque résultat persistant. Une équipe peut alors savoir si une anomalie vient du modèle, du prompt, du contrat de sortie ou d’une règle de validation modifiée. Pour les traitements continus, les échantillons de régression doivent être rejoués avant toute mise en production d’un changement.

Dans un pipeline de développement Apple ou un traitement de lots nécessitant une exécution régulière, une équipe peut choisir un Mac local, une machine distante temporaire ou un environnement Mac permanent selon la durée des tâches, les dépendances d’outils et la nécessité d’un accès physique. Pour une campagne ponctuelle de tests de schémas, une session temporaire peut suffire ; pour une chaîne de validation quotidienne, un environnement distant stable évite de reconstruire le poste à chaque exécution. Les modalités d’accès et les conditions de support peuvent être vérifiées dans le centre d’aide de nuvcloud.

La liste de contrôle avant d’écrire dans une base ou d’appeler un outil

  • [ ] Le cas d’usage utilise Structured Outputs plutôt qu’une simple consigne demandant du JSON.
  • [ ] Le point d’API et le modèle choisis prennent en charge la forme de sortie configurée.
  • [ ] strict: true est activé lorsque l’application dépend d’un contrat fermé.
  • [ ] Le schéma utilise uniquement les constructions effectivement testées avec le SDK déployé.
  • [ ] Chaque objet possède une liste required explicite.
  • [ ] additionalProperties: false est utilisé lorsque les propriétés inattendues sont dangereuses.
  • [ ] Les informations absentes disposent d’une représentation légale comme null, autre ou revue_humaine.
  • [ ] Les classifications sensibles reposent sur des énumérations plutôt que sur du texte libre.
  • [ ] Les paramètres d’outil sont séparés du format de la réponse finale.
  • [ ] Les contrôles d’identité, d’autorisation et d’état de ressource sont exécutés après le parsing.
  • [ ] Les dates, montants, devises et identifiants font l’objet de règles sémantiques.
  • [ ] Les refus sont distingués des erreurs réseau, des violations de schéma et des troncatures.
  • [ ] La réponse brute, la raison d’échec et la version du schéma sont conservées.
  • [ ] Les cas normaux, limites, malveillants et de migration sont inclus dans la régression.
  • [ ] Une stratégie de revue humaine existe pour les réponses légalement structurées mais métierement ambiguës.

Quand un environnement Mac distant devient pertinent

Le choix de l’environnement d’exécution ne corrige pas un schéma mal conçu, mais il peut simplifier la validation répétée d’un pipeline qui dépend d’outils Apple, de projets Xcode, de scripts d’interface ou de traitements audio et vidéo. Une machine locale reste préférable lorsqu’un accès permanent aux périphériques, aux fichiers internes ou à une charge lourde et stable est requis.

À l’inverse, une session distante temporaire peut convenir pour vérifier une nouvelle version de SDK, exécuter une campagne de cas limites ou reproduire un échec sur une configuration Mac propre. Une équipe peut comparer les options disponibles et les conditions de commande dans la page d’accès Mac pour la région Est des États-Unis, puis consulter la présentation de nuvcloud avant de choisir une durée d’utilisation.

Le point décisif reste la nature du travail : une sortie Structured Outputs exige un contrat, des tests et des contrôles indépendants ; elle ne transforme pas une machine distante en système de confiance automatique. Le poste doit donc être choisi selon le cycle de développement, la fréquence des validations et les dépendances réellement nécessaires.

Pour une équipe qui exécute déjà ces tâches sur un poste Windows, Linux ou une machine partagée, les limites apparaissent souvent dans la reconstruction de l’environnement, les différences de SDK, l’accès distant irrégulier et la difficulté à reproduire exactement une panne. Ces solutions peuvent rester rationnelles pour une charge longue et maîtrisée, mais elles deviennent moins adaptées lorsque le besoin consiste à ouvrir rapidement un environnement Mac propre, tester une chaîne Apple ou faire tourner temporairement un pipeline de validation.

Dans ce cas, louer un environnement Mac via nuvcloud peut offrir une voie plus directe que l’achat d’une machine dédiée ou la maintenance d’un poste partagé : l’équipe évite l’immobilisation d’un matériel utilisé seulement pendant les campagnes de test, limite les écarts entre développeurs et peut réserver l’usage à la période où le pipeline Structured Outputs doit être vérifié. Pour un besoin permanent, intensif ou dépendant d’interfaces physiques, l’achat reste cependant plus cohérent ; pour une validation temporaire ou une reproduction contrôlée, la location mérite d’être comparée sur la durée réelle du projet.

Donnez à vos workflows d’IA un environnement Mac fiable

Louez un Mac mini distant pour développer, tester et exécuter vos pipelines de données et vos agents dans un environnement dédié.

Accédez à votre instance à distance grâce à VNC et poursuivez vos tâches techniques sans dépendre des ressources de votre ordinateur local.

Pour aller plus loin

Offre limitée →