Structured outputs et JSON mode, quelle différence ?
Que garantissent JSON mode, tool calling et structured outputs ? Obtenir du JSON d’OpenAI, Claude, Gemini et des agents de codage.
En bref. Il y a trois façons d’obtenir du JSON d’un LLM : demander du JSON au modèle (JSON mode), lui faire remplir les arguments d’un outil (tool calling) ou lier la réponse à un schéma (structured outputs). Le 20 octobre 2026, nous avons lu la documentation des API d’OpenAI, d’Anthropic et de Google, ainsi que la documentation en ligne de commande de Claude Code, de Codex d’OpenAI, de GitHub Copilot (la CLI et VS Code), de Cursor et d’Antigravity CLI de Google.
- Trois façons, trois garanties différentes. Le JSON mode ne donne que du JSON valide ; dans un appel d’outil strict, les arguments respectent le schéma ; les structured outputs lient la réponse elle-même au schéma. Aucune ne garantit que la réponse est juste.
- Sur Claude, l’ancienne méthode renvoie désormais une erreur. Sur Opus 5.5 et Sonnet 5.5, un
tool_choicequi force le modèle à appeler un outil renvoie une erreur 400. La voie recommandée par Anthropic : le strict tool use avecauto, ou les structured outputs. - Un même schéma ne se comporte pas pareil chez tous les fournisseurs. OpenAI et Anthropic rejettent une règle de schéma non prise en charge avec une erreur ; Gemini l’ignore. L’ordre des champs, la règle sur les champs obligatoires et les limites du schéma varient aussi d’un fournisseur à l’autre.
- Dans les agents de codage, le schéma se passe par une option. Claude Code et Antigravity CLI ont
--json-schema, Codex a--output-schema. La documentation de Copilot CLI et de Cursor CLI décrit une sortie JSON, mais ne mentionne aucune option de schéma.
Que sont les structured outputs, et en quoi diffèrent-ils du JSON mode ?
Si un programme doit lire la réponse d’un modèle, cette réponse doit arriver à chaque fois sous la même forme. Si un champ manque, si un nombre arrive sous forme de texte ou si le JSON s’arrête au milieu, le programme plante. Dans l’article sur le prompt de revue de code, nous avons expliqué comment demander un constat avec une citation exacte, et dans l’article sur le LLM-as-a-judge, comment enregistrer le verdict du juge ; dans les deux cas, c’est du code qui lit la réponse du modèle. Cet article porte sur la forme de cette réponse : les façons d’obtenir du JSON, ce que chacune garantit et ce qui a changé chez Claude cet automne.

Avec le JSON mode, vous demandez du JSON au modèle et l’API promet que la réponse sera du JSON valide. Elle ne promet pas quels champs elle contiendra. La documentation d’OpenAI le dit clairement : le JSON mode garantit que la réponse se parse sans erreur, pas qu’elle respecte un schéma.
Avec le tool calling (on dit aussi function calling), vous décrivez un outil au modèle. Quand le modèle veut appeler cet outil, il écrit ses arguments en JSON. Si l’outil n’est pas strict, c’est du best effort, pas une garantie : selon la documentation d’Anthropic, avec un outil non strict, le modèle peut écrire "2" au lieu de 2 ou omettre un champ obligatoire. Avec un outil strict, les arguments respectent le schéma.
Avec les structured outputs, vous donnez le schéma à la réponse elle-même. À chaque token, le modèle ne choisit que parmi les tokens qui respectent le schéma ; c’est ce qu’on appelle le constrained decoding. Le strict tool use applique la même technique aux arguments d’un outil. Le résultat est un JSON valide qui contient les champs du schéma.
Alors, lequel pour quoi ? La documentation de Google résume la distinction : les structured outputs servent à mettre en forme la réponse finale, le function calling à faire exécuter une action au milieu de la conversation. Si la réponse doit aller dans une base de données, prenez les structured outputs ; si le modèle veut lire un fichier ou lancer une commande, le tool calling.
Comment activer les structured outputs chez les trois fournisseurs ?
Les trois fournisseurs proposent les mêmes trois façons, mais les noms, l’activation et les limites diffèrent. Nous avons relevé le tableau dans la documentation de chaque fournisseur le 20 octobre 2026 :
| Fournisseur | Lier la réponse à un schéma | Arguments d’outil | JSON sans schéma | Règle non prise en charge | Forcer un outil |
|---|---|---|---|---|---|
| OpenAI | text.format : json_schema et strict: true | strict: true ; par défaut dans la Responses API quand c’est possible | JSON mode : json_object | Renvoie une erreur | required ou une seule fonction par son nom |
| Anthropic | output_config.format : json_schema | strict: true | Non précisé dans la documentation | Renvoie 400 | Opus 5.5 et Sonnet 5.5 : 400 ; Haiku 5.5 l’accepte mais saute la réflexion |
response_format ou response_json_schema | Mode validated | Non précisé dans la documentation | Le modèle l’ignore | Mode any |
Source : la documentation d’OpenAI, d’Anthropic et de Google, 20 octobre 2026. « Non précisé dans la documentation » : dans les pages que nous avons lues ce jour-là, nous n’avons trouvé aucune phrase qui décrive ce cas. Chez Google, response_format est un champ de l’Interactions API, response_json_schema un champ de generateContent. La Responses API d’OpenAI passe en mode strict, quand c’est possible, une fonction qui n’est pas déclarée strict ; dans Chat Completions, le mode strict n’est pas le défaut.
Chez OpenAI, vous activez les structured outputs dans la Responses API, dans text.format, avec type: "json_schema" et strict: true. La documentation pose deux conditions : chaque champ doit être required, et chaque objet doit porter additionalProperties: false. Pour un champ facultatif, vous ajoutez null à son type. Les limites du schéma sont écrites elles aussi : au maximum 5 000 champs, 10 niveaux d’objets imbriqués, 1 000 valeurs d’enum et 120 000 caractères pour l’ensemble des noms et des valeurs. Un mot-clé non pris en charge renvoie une erreur. La première requête avec un schéma est plus lente, car l’API traite le schéma ; les suivantes ne le sont pas.
En tool calling, strict: true fait respecter le schéma aux arguments, et OpenAI recommande de toujours l’activer. Forcer le modèle à appeler un outil reste possible chez OpenAI : si vous mettez tool_choice à required, le modèle appelle au moins une fonction ; si vous donnez le nom d’une fonction, il appelle exactement celle-là.
Chez Anthropic, les structured outputs ont deux moitiés : output_config.format pour la réponse (JSON outputs) et strict: true pour les outils (strict tool use). Les deux peuvent servir ensemble dans la même requête. Le schéma est compilé en une grammaire. C’est pourquoi la première requête avec un schéma donné est plus lente ; le schéma compilé est conservé 24 heures après sa dernière utilisation. Les limites s’appliquent par requête : au maximum 20 outils stricts, 24 champs facultatifs au total sur tous les schémas et 16 champs qui utilisent anyOf ou plusieurs types ; la compilation a aussi un délai maximal de 180 secondes. Une règle non prise en charge, comme minimum ou maxLength, renvoie une erreur 400. Les champs arrivent dans l’ordre du schéma, mais les champs obligatoires sont écrits avant les facultatifs. Encore un détail : modifier output_config.format invalide le cache de prompt de cette conversation. Nous avons expliqué pourquoi le cache se casse dans l’article sur le prompt caching.
Le principal changement de cet automne concerne aussi Anthropic. Une façon connue d’obtenir du JSON de Claude consistait à définir un seul outil et à forcer le modèle à l’appeler avec tool_choice. Selon les notes de version, sur Opus 5.5, sorti le 22 septembre, et sur Sonnet 5.5, sorti le 28 septembre, les types any et tool de tool_choice renvoient une erreur 400. La voie recommandée par Anthropic est le strict tool use avec auto, et les structured outputs si vous avez besoin d’une forme JSON fixe. Selon la documentation des outils, les modèles dont la réflexion est activée par défaut, comme Haiku 5.5, acceptent encore le forçage, mais lors d’un appel forcé le modèle appelle l’outil directement, sans réfléchir. Sur Haiku 5.5, sorti le 7 octobre, une autre ancienne méthode, écrire vous-même le début du message de l’assistant (prefill), peut renvoyer une erreur 400 ; de toute façon, les structured outputs ne fonctionnent pas avec le prefill.
Chez Google, le guide actuel décrit l’Interactions API : vous donnez à response_format un mime_type: "application/json" et un schema. Dans generateContent, on fait la même chose avec response_json_schema. Selon la documentation, le modèle écrit un JSON syntaxiquement valide qui respecte le schéma et range les champs dans l’ordre du schéma ; l’API peut refuser les schémas très grands ou très imbriqués. Il y a là une différence importante : OpenAI et Anthropic rejettent avec une erreur une règle de schéma qu’ils ne prennent pas en charge, alors que Gemini l’ignore. Si votre schéma contient une règle que Gemini ne prend pas en charge, la requête réussit quand même, mais la règle n’est pas appliquée. En function calling, le mode validated fait respecter le schéma aux arguments ; le mode any, lui, force le modèle à appeler une fonction à chaque fois.
Que ne garantissent pas les structured outputs ?
Un schéma garantit la forme de la réponse, pas son contenu. Google l’écrit clairement : rien ne garantit que les valeurs soient correctes sur le fond. OpenAI dit aussi que les structured outputs peuvent encore contenir des erreurs, et ajoute un avertissement : si l’entrée n’a rien à voir avec le schéma, le modèle peut inventer en essayant de s’y conformer. Anthropic, de son côté, énumère les cas où le schéma peut ne pas tenir du tout. Ce qu’il faut vérifier :

Les deux cas les plus fréquents sont le refus et la réponse tronquée. Chez Anthropic, une requête refusée pour des raisons de sécurité n’arrive pas comme une erreur mais comme une réponse normale portant stop_reason: "refusal" : elle renvoie HTTP 200, les tokens générés sont facturés et la réponse peut ne pas respecter le schéma. Chez OpenAI, le refus arrive dans un champ refusal à part. Une réponse qui bute sur max_tokens reste incomplète chez les deux fournisseurs ; Anthropic conseille de réessayer avec un max_tokens plus élevé. Votre code doit donc d’abord regarder pourquoi la réponse s’est terminée, et seulement ensuite parser le JSON.
Un piège propre à Claude est le champ de réflexion dans le schéma. Mettre un champ reasoning dans le schéma et demander au modèle d’y réfléchir avant de répondre peut provoquer un refus chez Claude. La documentation d’Anthropic sur les refus range un champ nommé reasoning, thinking ou trace, dans une sortie JSON ou dans l’entrée d’un outil, parmi les causes fréquentes d’un refus de type reasoning_extraction. Elle conseille plutôt de demander une courte explication, les preuves sur lesquelles repose le résultat ou un résumé de ce qui a été fait. Depuis le 24 septembre, un refus de ce type qui arrive avant toute sortie est lui aussi facturé. La règle est propre à ce fournisseur : l’un des exemples du guide d’OpenAI est un schéma qui donne la réponse avec des explications étape par étape. Chez Claude, c’est le réglage d’effort, et non le schéma, qui décide combien le modèle réfléchit ; nous l’avons expliqué dans l’article sur l’effort.
La solidité du schéma entre les mains du fournisseur est une autre question. Dans nos propres relevés, nous avons envoyé un schéma JSON strict à un endpoint que le catalogue d’un service de routage présentait comme compatible avec response_format, mais pas avec les structured outputs. La réponse n’était pas une erreur : HTTP 200 et 1 639 caractères de markdown. Et le contenu était juste : il nommait le bug réel du diff utilisé pour la requête de test. Il ne manquait que le JSON. C’est le relevé d’une seule requête de test, pas un taux. Mais il montre que la promesse de schéma d’une API ne remplace pas la vérification dans le code qui lit la réponse. Dans diffox, une réponse qui ne peut pas être parsée comme du JSON ne compte pas comme un succès ; l’appel échoue et entre dans le journal de la review, avec l’agent ou l’étape qui est tombé et la raison.
Comment obtenir du JSON d’un agent de codage en ligne de commande ?
La même question se pose avec les agents de codage. Si vous lancez un agent en CI ou dans un script et transmettez son résultat à une autre étape, vous devez pouvoir compter sur la forme de la réponse. Les cinq outils se lancent en ligne de commande, mais ils ne donnent pas tous la même chose : certains mettent la réponse dans une enveloppe JSON, d’autres demandent la réponse elle-même selon un schéma. Nous avons relevé le tableau dans la documentation de chaque outil le 20 octobre 2026 :
| Outil | Commande | Sortie | Schéma |
|---|---|---|---|
| Claude Code | claude -p --output-format json --json-schema '…' | Enveloppe JSON ; le résultat est dans le champ structured_output | Valide la sortie ; si elle ne correspond pas, redemande, et renvoie une erreur à la limite de tentatives |
| Codex | codex exec --output-schema schema.json | Événements JSONL avec --json ; -o écrit le dernier message dans un fichier | Demande une réponse finale conforme au schéma ; comment c’est appliqué n’est pas précisé dans la documentation |
| GitHub Copilot CLI | copilot -p '…' --output-format json | Événements JSONL | Non précisé dans la documentation |
| Copilot, VS Code | code chat '…' | Ouvre le chat dans l’éditeur | Non précisé dans la documentation |
| Cursor CLI | agent -p --output-format json | Une seule enveloppe JSON ; la réponse est du texte dans le champ result | Non précisé dans la documentation |
| Antigravity CLI | agy -p '…' --output-format json --json-schema '…' | Enveloppe JSON ; le résultat est dans le champ structured_output | Applique le schéma ; comment, ce n’est pas précisé dans la documentation |
Source : la documentation de chaque outil, 20 octobre 2026. « Non précisé dans la documentation » : dans les pages que nous avons lues ce jour-là, nous n’avons trouvé aucune phrase qui décrive ce cas. Le fait que Claude Code valide la sortie par rapport au schéma et redemande est décrit dans la documentation de l’Agent SDK ; claude -p exécute ce SDK en ligne de commande. Dans Cursor CLI, si l’exécution échoue, aucun JSON n’est écrit et le processus se termine avec un code différent de zéro.
La distinction du tableau compte. Une enveloppe JSON veut dire que la réponse arrive dans un objet JSON ; la réponse qu’il contient reste du texte libre. Si l’étape suivante doit lire des champs de la réponse, il vous faut soit un outil avec une option de schéma, soit une étape qui valide la réponse dans le code. La documentation de Claude Code est claire là aussi : même si le résultat a l’air réussi, traitez-le comme un échec quand le champ structured_output manque. Claude Code précise un autre détail : il traite le mot-clé format d’un schéma, par exemple "format": "email", comme une simple annotation et ne l’applique pas.
Si vous utilisez encore Gemini CLI, gemini -p --output-format json renvoie la réponse sous forme de texte dans le champ response ; cette documentation ne mentionne pas d’option de schéma. Pour les utilisateurs du niveau gratuit et de Google One, Google a remplacé Gemini CLI par Antigravity CLI le 18 juin 2026 ; avec des clés d’API payantes, Gemini CLI reste utilisable.
Comment obtenir du JSON à la fin d’une boucle d’agent ?
Dans une boucle d’agent, le modèle lit, cherche et essaie d’abord avec des outils ; tout à la fin, il donne un résultat. Pour que ce résultat soit du JSON chez Claude, il suffisait de forcer le modèle à appeler un outil « submit » à la dernière étape. Comme cette voie renvoie une erreur 400 sur Opus 5.5 et Sonnet 5.5, la démarche est désormais la suivante :
- Laissez
tool_choicesurauto. Selon Anthropic, le prompt influence toujours le choix de l’outil, et un modèle qui n’est pas forcé peut réfléchir avant d’appeler l’outil. - Donnez au modèle un dernier outil avec
strict: true, par exemplesubmit, et demandez-lui dans le prompt de l’appeler exactement une fois, en dernier, quand le travail est terminé. Comme les arguments respectent le schéma, vous n’avez pas besoin de parser le résultat à part. - Arrêtez la boucle quand cet appel arrive. Si le modèle s’arrête sans appeler l’outil, si la réponse est tronquée à
max_tokensou si un refus arrive, ne le comptez pas comme un résultat ; levez une erreur explicite. - Limitez les appels d’outils. Une fois la limite atteinte, au lieu d’exécuter l’outil, dites au modèle de décider maintenant. Plafonnez aussi le nombre de tours.
- Si vous voulez une justification, mettez dans le schéma un court champ
evidence, pasreasoning.
Pour la réponse finale, vous pouvez aussi utiliser les structured outputs au lieu d’un outil : la documentation d’Anthropic indique que les outils stricts et output_config.format fonctionnent ensemble dans la même requête. Les appels d’outils respectent le schéma, et la réponse texte finale est elle aussi liée au schéma. L’exemple ci-dessous montre la première voie, avec le SDK Python d’Anthropic :
# Exemple : tool_choice reste sur auto, la boucle s’arrête à l’appel de submit.
import anthropic
client = anthropic.Anthropic()
SUBMIT = {
"name": "submit",
"description": "Give the final answer. Call exactly once, last.",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"verdict": {"type": "string", "enum": ["HOLDS", "REFUTED", "UNSURE"]},
"evidence": {"type": "string"},
},
"required": ["verdict", "evidence"],
"additionalProperties": False,
},
}
def run(model, messages, tools, run_tool, max_calls=8):
calls = 0
for _ in range(max_calls + 3):
response = client.messages.create(
model=model, max_tokens=8000, tools=tools + [SUBMIT], messages=messages)
if response.stop_reason in ("max_tokens", "refusal"):
raise RuntimeError("no answer: " + response.stop_reason)
messages.append({"role": "assistant", "content": response.content})
uses = [b for b in response.content if b.type == "tool_use"]
for b in uses:
if b.name == "submit":
return b.input # le résultat, conforme au schéma
if not uses:
raise RuntimeError("the model stopped without calling submit")
results = []
for b in uses:
calls += 1
out = (run_tool(b.name, b.input) if calls <= max_calls
else "Tool budget spent: call submit now.")
results.append({"type": "tool_result", "tool_use_id": b.id, "content": out})
messages.append({"role": "user", "content": results})
raise RuntimeError("turn limit reached without submit")Dans diffox, l’étape qui vérifie un constat par rapport au dépôt lui-même, avant de l’écrire dans la pull request, fonctionne de cette façon. La note dans le code le dit clairement : « A MANUAL LOOP, tool_choice left at auto ». Le modèle peut faire au plus 8 appels avec des outils git en lecture seule, et le prompt lui demande d’appeler l’outil submit_verdict exactement une fois, en dernier, quand il a terminé. submit_verdict est un outil strict : le verdict est l’une de trois valeurs (HOLDS, REFUTED, UNSURE), accompagnée d’un champ de preuves ; le schéma n’a pas de champ de réflexion. Une fois la limite atteinte, l’outil ne s’exécute plus et on demande au modèle de rendre son verdict ; les tours s’arrêtent 3 au-delà de la limite. Si le modèle s’arrête sans rendre de verdict, « pas de verdict » est inscrit au journal et le constat reste tel quel : aucun constat n’est considéré comme réfuté par défaut.
Lequel utiliser, et dans quel cas ?
La règle est simple : choisissez selon ce dont a besoin celui qui lira le JSON, et vérifiez quand même chaque réponse reçue. Un résumé pour les cas courants :

Le point commun des cartes : le schéma garantit la forme, le reste revient à votre code. Les règles qu’un schéma ne sait pas exprimer, comme la longueur d’un texte, la plage d’un nombre ou la cohérence d’un champ avec un autre, se vérifient dans le code une fois la réponse arrivée. Si la vérification échoue, redemandez une fois ; si le second essai échoue aussi, n’avalez pas l’erreur, levez-la explicitement. Le Claude Agent SDK fonctionne de la même façon : il redemande une sortie non conforme et renvoie une erreur une fois la limite de tentatives atteinte. Nous avons décrit ce genre de vérifications, qui s’exécutent après la réponse du modèle, dans l’article sur le harness engineering.
Questions fréquentes
Que sont les structured outputs ?
Une fonctionnalité d’API qui lie la réponse d’un LLM au schéma JSON que vous fournissez. Pendant qu’il génère des tokens, le modèle ne peut pas sortir du schéma (constrained decoding) ; la réponse arrive en JSON valide, avec les champs du schéma. On l’active chez OpenAI dans text.format avec json_schema et strict: true, chez Anthropic avec output_config.format et chez Google avec response_format ou response_json_schema. Le schéma garantit la forme de la réponse, pas que son contenu soit juste.
Quelle différence entre le JSON mode et les structured outputs ?
Le JSON mode garantit seulement que la réponse est du JSON valide ; il ne garantit pas quels champs elle contiendra. Les structured outputs lient la réponse au schéma que vous fournissez : les champs obligatoires ne manquent pas, les types et les valeurs d’enum sont ceux du schéma. OpenAI propose les deux et recommande les structured outputs quand ils sont pris en charge. Chez OpenAI, si le JSON mode est activé et que le mot « JSON » n’apparaît pas dans le prompt, l’API renvoie une erreur.
Quelle différence entre le function calling et les structured outputs ?
Le function calling, c’est le modèle qui appelle un outil pour faire exécuter une action ; le JSON, ce sont les arguments de cet outil. Les structured outputs, eux, sont la forme de la réponse finale du modèle. La documentation de Google trace la limite ainsi : les structured outputs pour mettre en forme la réponse finale, le function calling pour faire exécuter une action au milieu de la conversation. Pour que les arguments d’outil respectent eux aussi le schéma, on utilise strict: true chez OpenAI et Anthropic, et le mode validated dans Gemini.
Pourquoi tool_choice any renvoie-t-il une erreur sur Claude ?
Sur Opus 5.5 et Sonnet 5.5, les types de tool_choice qui forcent le modèle à appeler un outil, any et tool, renvoient une erreur 400. La voie recommandée par Anthropic est de laisser tool_choice sur auto et d’utiliser le strict tool use, et les structured outputs si vous avez besoin d’une forme JSON fixe. Le prompt influence toujours l’outil appelé. Haiku 5.5 accepte encore le forçage, mais lors d’un appel forcé le modèle appelle l’outil directement, sans réfléchir.
Comment donner un schéma JSON à Gemini ?
Dans l’Interactions API, vous donnez à response_format un type: "text", un mime_type: "application/json" et un schema ; dans generateContent, on fait la même chose avec response_json_schema (responseFormat.text.schema en REST). Gemini prend en charge un sous-ensemble de JSON Schema et ignore les règles qu’il ne prend pas en charge ; OpenAI et Anthropic, eux, renvoient une erreur pour une telle règle. Google recommande de valider quand même les valeurs de la réponse dans votre application.
Les structured outputs garantissent-ils que le contenu est juste ?
Non. Google et OpenAI le disent clairement dans leur documentation : le schéma garantit la forme de la réponse, pas que les valeurs soient justes. Anthropic énumère aussi les cas où le schéma peut ne pas tenir : un refus, une réponse tronquée à max_tokens et la casse des valeurs d’enum. Regardez d’abord pourquoi la réponse s’est terminée, puis validez la réponse dans le code ; si elle ne passe pas, redemandez une fois.
Comment diffox obtient-il du JSON d’un modèle ?
diffox est un outil de revue de code par IA, et il demande aux modèles leurs réponses avec un schéma JSON. L’étape qui vérifie un constat par rapport au dépôt lui-même est une boucle d’agent : tool_choice est sur auto, les appels d’outils sont limités à 8 et la boucle se termine par un outil strict submit_verdict. Une réponse qui ne peut pas être parsée comme du JSON ne compte pas comme un succès ; elle entre comme erreur dans le journal de la review. Si le modèle s’arrête sans rendre de verdict, le constat reste tel quel.