Tutoriel

Comment faire produire du JSON à l’API Gemini ? Tutoriel Structured Output et JSON Schema

Arrêtez de supplier le modèle de « ne renvoyer que du JSON ». Fixez la forme avec Structured Output, contraignez les champs avec JSON Schema, et le code aval pourra parser sereinement.

Vous demandez du JSON à Gemini et vous recevez un préambule, une virgule manquante, ou des noms de champs plus aimables. Dire au prompt « JSON uniquement » baisse les chances ; ce n’est pas un contrat. Structured Output déplace le contrat dans le décodage : déclarez un type MIME, attachez un schéma, et le modèle émet des jetons dans cette forme. Après ce guide vous saurez choisir mode JSON vs mode Schema, écrire une requête qui marche, et valider quand même en local.

Pourquoi une sortie structurée

Une fois que l’aval appelle JSON.parse, l’échec n’est plus un problème de copie — tout le pipeline s’arrête. Les classifieurs ont besoin d’enums figés, les extracteurs de clés stables, les appels d’outils d’objets de paramètres. Les réponses libres coûtent cher : un JSON cassé, c’est des retries, des logs, ou un clic de plus de l’utilisateur.

Un échec plus discret : « ça parse, mais la forme est fausse ». Vous attendiez items comme tableau et vous avez reçu un objet ; vous attendiez score comme nombre et vous avez reçu "0.9". Le code lit undefined, et le bug explose bien plus tard. Structured Output corrige la forme, pas la vérité : vous obtenez du JSON légal, calé sur le schéma, pas une catégorie garantie. La production a encore besoin de contrôles métier — le parsing, lui, se tait.

Voir les notes officielles de capacité dans la documentation Gemini Structured output. Google a aussi annoncé davantage de mots-clés JSON Schema et l’ordre des propriétés ; lisez la mise à jour Structured Outputs. Avant d’écrire un schéma, parcourez Understanding JSON Schema pour ne pas confondre « mots-clés que la spec autorise » et « mots-clés que ce modèle applique vraiment ».

Mode JSON vs mode Schema

Imaginez deux interrupteurs. Le premier promet seulement « cette chaîne parse comme du JSON ». Le second promet « ce JSON correspond au schéma que vous avez déclaré ». Si votre code lit des champs nommés, prenez le second. Le premier, seulement pour une extraction exploratoire où le modèle invente aussi les clés.

Niveau Ce que vous configurez Ce que vous obtenez vraiment
Mode JSON Uniquement responseMimeType: application/json Du JSON généralement valide ; noms et imbrication restent au choix du modèle
Mode Schema Type MIME + responseSchemaouresponseJsonSchema Forme, types, champs requis et enums suivent le schéma

Avec le mode JSON seul, la doc le traite encore comme un indice fort, avec un petit risque de sortie malformée. Pour approcher « toujours parser comme un objet », envoyez aussi un schéma. Le schéma compte dans les jetons d’entrée : ne recolliez pas la même description dans le prompt. La duplication nuit à la qualité et au quota.

responseSchema ou responseJsonSchema

responseSchema utilise un sous-ensemble de schéma façon OpenAPI 3.0. Les noms de types REST sont souvent en majuscules, comme OBJECT, STRING. Il convient aux objets plats, à la classification par enum, et à figer l’ordre des clés avec propertyOrdering. Il ne comprend pas $ref / $defs, donc arbres récursifs et définitions partagées doivent être inlinés et explosent vite.

responseJsonSchema vise Gemini 2.5 et plus récent, et parle un JSON Schema plus proche du standard, couvrant anyOf, $ref, minimum / maximum, additionalProperties, type: null, prefixItems, et davantage. Générer le schéma depuis Pydantic ou Zod réduit les frottements. Les modèles plus récents gardent l’ordre des clés tel que déclaré, ce qui aide les diffs de logs et les tests golden.

Trois règles de pouce. Classer/extraire à plat : n’importe lequel des deux champs. Récursion, définitions partagées ou unions : préférez responseJsonSchema. S’il faut figer l’ordre des champs, confirmez que l’endpoint honore encore propertyOrdering ; ne supposez pas que chaque surface d’API se comporte pareil.

Comment écrire le schéma

Décrivez l’objet que vous lirez vraiment, pas un modèle du monde complet. Chaque champ optionnel de plus est une chance de remplir du bruit. Mettez les noms requis dans required, les enums dans enum, les bornes numériques dans minimum / maximum. Ajouter description sur les propriétés est souvent plus stable que de les réexpliquer dans le prompt, parce que la contrainte voyage avec le décodage.

Le schéma ci-dessous modélise une classification de tickets : category est l’une de trois valeurs, priority un entier, summary une chaîne. Voilà ce que le mode Schema doit posséder — la forme, pas le fait que le ticket soit vraiment urgent.

{
  "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
}

Les tableaux utilisent items pour les éléments. Pour des listes fixes façon tuple, voyez prefixItems sur le chemin JSON Schema. Ne traitez pas « absent de required » comme nullable — autorisez explicitement null, sinon le modèle peut omettre la clé alors que votre code suppose que obj.field existe toujours.

Inlinez les objets imbriqués. N’allez chercher $defs + $ref que lorsque la même structure apparaît une troisième fois, ou qu’un nœud d’arbre se référence lui-même. L’abstraction trop tôt rend les rejets illisibles : vous déboguez un blob expansé quand le serveur refuse le schéma.

Python et JavaScript en pratique

Les extraits suivent des formes SDK officielles courantes. Remplacez l’id du modèle par le SKU 2.5 / plus récent que votre projet a vraiment — ne traitez pas le nom d’exemple comme un pin de production figé.

Python : type MIME + JSON Schema

from google import genai

client = genai.Client()
schema = {
    "type": "object",
    "properties": {
        "category": {"type": "string", "enum": ["billing", "bug", "feature"]},
        "priority": {"type": "integer"},
        "summary": {"type": "string"},
    },
    "required": ["category", "priority", "summary"],
}

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Classify this ticket: invoice PDF cannot be downloaded.",
    config={
        "response_mime_type": "application/json",
        "response_json_schema": schema,
    },
)
print(response.text)

Si l’équipe modèle déjà avec Pydantic, passez Model.model_json_schema() à response_json_schema, puis model_validate_json(response.text) pour un second contrôle local. La couche une est la forme API ; la couche deux, c’est votre système de types qui refuse des valeurs légales en apparence mais absurdes, comme priority 99.

JavaScript : generationConfig

const response = await ai.models.generateContent({
  model: "gemini-2.5-flash",
  contents: "Classify this ticket: invoice PDF cannot be downloaded.",
  config: {
    responseMimeType: "application/json",
    responseJsonSchema: {
      type: "object",
      properties: {
        category: { type: "string", enum: ["billing", "bug", "feature"] },
        priority: { type: "integer" },
        summary: { type: "string" },
      },
      required: ["category", "priority", "summary"],
    },
  },
});
const data = JSON.parse(response.text);

Les appels REST mettent les mêmes champs sur generationConfig. Le responseSchema style OpenAPI utilise encore des types en majuscules sur certains endpoints — ne le mélangez pas avec le object minuscule de JSON Schema. Parsez tout de suite avec JSON.parse ; ne raclez pas un bloc de code fencé avec une regex. Vous avez déclaré un MIME JSON, traitez donc tout le corps comme du JSON.

Pièges fréquents

  • Écrire la structure deux fois — une dans le prompt, une dans le schéma — fait osciller le modèle entre les deux descriptions.
  • Schémas trop gros : imbrication profonde, $ref trop profond, ou anyOf trop large peuvent être rejetés ou faiblement appliqués. Livrez d’abord un petit objet, puis agrandissez.
  • Ne déléguez pas la vérité au schéma. Les enums bornent l’ensemble ; ils n’empêchent pas un mauvais choix. Contrôlez par échantillon ou ajoutez des règles sur les chemins critiques.
  • Les types locaux dérivent du schéma de la requête. Vous changez Pydantic/Zod et oubliez le schéma du payload ; la production gagne des clés en silence ou en perd d’anciennes.
  • Ne testez pas seulement le chemin heureux. Ajoutez tableaux vides, champs nullable, chaînes longues, et enums illégaux (qui doivent être bloqués).

Encore un sujet d’ingénierie : les logs ne doivent pas stocker seulement response.text. Enregistrez l’id du modèle, le hash du schéma et la version du prompt. Les casses de Structured Output, c’est souvent « les défauts ont changé » ou « une micro-édition de schéma a été rejetée ». Sans ces trois-là, vous jurerez que ça marchait hier.

Livrer : valider, comparer, réessayer

Traitez le corps API comme des octets non fiables. Parsez, validez contre le même schéma, puis mappez vers les types internes. En cas d’échec, loguez le texte brut (masquez les secrets) et choisissez retry vs dégradation. Ne réessayez pas avec un schéma totalement différent, sinon vous ne saurez pas départager le bruit du modèle d’un contrat qui bouge.

Pendant le débogage, collez des échantillons dans les outils du site. Formatage JSON pour voir l’imbrication, Validation JSON pour attraper la syntaxe, puis déposez le contrat dans JSON Schema pour voir si l’instance passe. Quand des champs apparaissent ou disparaissent, JSON Diff deux réponses au lieu de scanner les logs à l’œil.

Quand vous concevez un pipeline d’extraction, écrivez à la main une « sortie idéale », inférez les types avec l’outil Schema, et recollez ce schéma dans la requête Gemini. Le contrat vit alors à un seul endroit : un document que vous pouvez tester, pas un accord verbal dans l’historique de chat.

FAQ

application/json suffit-il à lui seul ?

Suffisant pour explorer. Dès que le code lit des champs fixes, envoyez aussi un schéma. Sinon vous obtenez de la prose en forme de JSON, pas une API.

Structured Output peut-il remplacer l’appel de fonctions ?

Non. L’appel de fonctions laisse le modèle choisir un outil et remplir les arguments. Structured Output contraint la forme de cette réponse. Besoin d’exécuter du code ou d’appeler une API externe ? Utilisez les outils. Besoin d’un blob typé ? Structured Output, et vous évitez un aller-retour.

Pourquoi mon schéma a-t-il été rejeté ?

En général des mots-clés non supportés sur cet endpoint, une récursion trop profonde, ou un mélange des dialectes responseSchema et responseJsonSchema. Réduisez à un objet de trois champs, prouvez que ça marche, puis ajoutez.

La sortie est-elle toujours correcte ?

Non. La forme peut être valide et les faits faux. Argent, e-mails et catégories de tickets ont encore besoin de règles ou d’un échantillonnage humain.

Conclusion et suite

Un JSON fiable ne vient pas d’un plus long « s’il vous plaît, JSON uniquement ». Il vient d’un type MIME plus un schéma. Pour des jobs plats, responseSchemaouresponseJsonSchema convient ; pour $ref, les unions, ou les schémas générés depuis Pydantic/Zod, prenez le second. Validez encore en local, et transformez les échecs en tests de régression avec format, Schema et Diff.

Suite : prenez l’endpoint le plus fragile que vous avez et faites-en « un schéma, une requête, une validation locale ». Calmez d’abord ce chemin, puis copiez le motif.