Lesson 1 — Sorties structurées

Du texte libre au JSON garanti

5 min read

This content is not translated yet. Showing the original version.

Du texte libre au JSON garanti

Le problème : un JSON « à peu près »

Demandez à un LLM « donne-moi en JSON… » : il répond souvent bien, mais pas toujours pareil. Il ajoute une phrase d'introduction, entoure le JSON de balises Markdown, choisit lui-même les noms de champs, met "urgence": true une fois et "urgence": "oui" la suivante. Pour un programme qui doit lire ce JSON, chacun de ces écarts est une panne.

flowchart LR
  subgraph L["Sans schéma"]
    P1["Prompt : réponds en JSON"] --> G1["Gemini"] --> T1["Texte + json entre balises, champs au hasard"]
    T1 --> X1["json.loads : erreur"]
  end
  subgraph S["Avec response_schema"]
    P2["Prompt + schéma"] --> G2["Gemini : décodage contraint"] --> T2["JSON exact : champs, types, valeurs permises"]
    T2 --> X2["Pydantic : objet Python"]
  end

Les deux paramètres

  • response_mime_type="application/json" : le modèle ne produit que du JSON, sans texte autour.
  • response_schema=MonModele : la forme du JSON est imposée (noms des champs, types, champs obligatoires, valeurs permises). On parle de décodage contraint : à chaque mot généré, le modèle ne peut choisir que des suites compatibles avec le schéma.
exemple (utilise files/schemas.py)
from google import genai
from google.genai import types
from schemas import Commande

client = genai.Client()
r = client.models.generate_content(
    model="gemini-3.5-flash",
    contents="Extrais la commande de cet e-mail :\n" + texte_email,
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=Commande,      # une classe Pydantic
        temperature=0))
print(r.text)        # le JSON brut
print(r.parsed)      # un objet Commande, déjà construit par le SDK

temperature=0 rend la sortie aussi constante que possible : en extraction, on ne veut pas de créativité.

ApprocheJSON valide ?Champs prévisibles ?Valeurs contrôlées ?
« Réponds en JSON » dans le promptsouventnonnon
response_mime_type seulouinonnon
response_mime_type + response_schemaouiouioui (types, énumérations)
… + validation Pydantic (module 2)ouiouioui, plus vos règles métier

Avec response_mime_type="application/json" mais SANS response_schema, qu'est-ce qui est garanti ?

Pourquoi temperature=0 en extraction ?

Exercice : un schéma pour une facture

Écrivez une classe Pydantic Facture avec : fournisseur (texte), numero (texte), montant_ttc (nombre décimal positif ou nul), devise limitée à EUR ou USD, et date_echeance facultative. Appelez Gemini sur un texte de facture inventé.

Voir la solution
facture.py
from datetime import date
from typing import Literal, Optional

from google import genai
from google.genai import types
from pydantic import BaseModel, Field


class Facture(BaseModel):
    fournisseur: str
    numero: str
    montant_ttc: float = Field(ge=0)
    devise: Literal["EUR", "USD"]
    date_echeance: Optional[date] = Field(None, description="AAAA-MM-JJ, null si absente")


texte = "Facture F-2026-118 de Papeterie Durand, total TTC 241,80 €, à régler avant le 30/11/2026."
client = genai.Client()  # gardez le client dans une variable (voir plus bas)
r = client.models.generate_content(
    model="gemini-3.5-flash", contents="Extrais cette facture :\n" + texte,
    config=types.GenerateContentConfig(response_mime_type="application/json",
                                       response_schema=Facture, temperature=0))
print(r.parsed)

ge=0 plutôt que gt=0, pour la raison vue plus haut. Et piège vérifié en testant cette solution : écrire genai.Client().models.generate_content(...) d'une traite échoue avec RuntimeError: Cannot send a request, as the client has been closed. Le client temporaire est fermé dès qu'il n'est plus référencé ; gardez-le dans une variable. Literal["EUR", "USD"] devient une liste de valeurs permises dans le schéma.

Résultat obtenu

Le même e-mail de commande (cmd-02 du jeu annoté), avec et sans schéma, gemini-3.5-flash, le 2026-10-15 :

sortie réelle, 2026-10-15 (tableau lignes condensé)
--- SANS SCHÉMA ---
Voici le résultat au format JSON :

```json
{
  "client": {
    "nom": "Paul Girard",
    "entreprise": "Salon de thé Les Glycines"
  },
  "lignes": [ { "reference": "THE-200", "quantite": 6 }, { "reference": "TIS-100", "quantite": 1 } ],
  "urgence": true
}
```
json.loads : JSONDecodeError Expecting value: line 1 column 1 (char 0)
--- AVEC response_schema ---
{"confiance":1,"champs_incertains":[],"client":"Salon de thé Les Glycines","contact":"Paul Girard","lignes":[{"reference":"THE-200","quantite":6},{"reference":"TIS-100","quantite":1}],"date_livraison":"2026-10-14","urgence":"urgente","mode":"retrait"}
type de r.parsed : Commande | lignes : [Ligne(reference='THE-200', quantite=6), Ligne(reference='TIS-100', quantite=1)]

Sans schéma, le contenu est juste mais inexploitable : phrase d'introduction, balises Markdown, structure inventée, urgence booléenne. Avec le schéma, le JSON est directement lisible, la correction « finalement mettez-en 6 » est prise en compte, et la date relative « demain » est convertie en 2026-10-14.

Je sais faire

  • Expliquer pourquoi « réponds en JSON » ne suffit pas
  • Utiliser response_mime_type et response_schema avec une classe Pydantic
  • Lire r.text et r.parsed
  • Reconnaître une contrainte refusée par le schéma Gemini et la reformuler
Next