ITSkillsCenter
Intelligence Artificielle

Sorties structurées JSON avec Gemini et Pydantic en Python

13 دقائق للقراءة

📍 Guide principal : Développer avec l’API Google Gemini — les sorties structurées sont la clé pour exploiter le modèle dans un programme.

Tant qu’on lit les réponses du modèle avec ses yeux, le texte libre convient. Mais dès qu’un programme doit les exploiter — enregistrer en base, remplir un formulaire, déclencher un traitement — le texte libre devient un cauchemar : il faut le parser avec des expressions régulières fragiles qui cassent au premier changement de formulation. Les sorties structurées résolvent cela à la racine : on impose au modèle un schéma, et il répond par un JSON qui s’y conforme, directement transformable en objet de votre langage. Ce tutoriel montre comment forcer ce format et le valider, avec Pydantic côté Python.

Ce que vous allez apprendre

  • Comprendre pourquoi parser du texte libre est fragile, et ce que le JSON structuré change.
  • Définir un schéma de données avec Pydantic.
  • Forcer Gemini à répondre selon ce schéma via la configuration de génération.
  • Valider et exploiter la réponse comme un objet Python typé.
  • Gérer les structures imbriquées, les listes et les valeurs imposées.

Ce que vous allez construire

Un extracteur de factures fiable : à partir d’un texte de facture (ou d’une image, en réutilisant le tutoriel multimodal), il produit un objet structuré — commerçant, date, lignes d’articles, total — que votre code manipule sans aucune analyse de chaîne. Le genre de brique qu’on branche ensuite sur une base de données ou une comptabilité.

Prérequis

  • L’environnement du premier tutoriel avec google-genai.
  • La bibliothèque Pydantic : pip install pydantic.
  • Des notions de classes Python.
  • ⏱️ Temps estimé : environ 25 minutes.

Étape 1 — Le problème du texte libre

Imaginez demander à un modèle « extrais le commerçant et le total de cette facture ». Une fois il répond « Le commerçant est Alpha Informatique et le total s’élève à 75 000 FCFA. » ; une autre fois « Commerçant : Alpha Informatique — Total : 75000 ». Les deux sont correctes pour un humain, mais votre code, lui, doit deviner où commence et finit chaque valeur. Vous écrivez une expression régulière, elle marche sur le premier cas, casse sur le second. Multipliez par dix champs et vous passez plus de temps à parser qu’à construire.

La sortie structurée renverse le problème. Plutôt que d’extraire la donnée d’une phrase, on impose au modèle de produire directement la donnée dans une forme convenue. On ne parse plus : on charge.

Étape 2 — Décrire les données avec Pydantic

Pydantic permet de décrire une structure de données comme une classe Python, avec des champs typés. Cette classe sert à la fois de contrat envoyé au modèle et de validateur de la réponse. Définissons la forme d’une facture.

from pydantic import BaseModel

class LigneArticle(BaseModel):
    nom: str
    quantite: int
    prix_unitaire: float

class Facture(BaseModel):
    commercant: str
    date: str
    articles: list[LigneArticle]
    total: float

Lisez cette structure comme un formulaire vide. Une Facture a un commerçant (texte), une date (texte), une liste de lignes d’articles, et un total (nombre décimal). Chaque LigneArticle a un nom, une quantité entière et un prix unitaire. Les types ne sont pas décoratifs : ils disent au modèle qu’une quantité est un entier et un total un décimal, et ils permettront à Pydantic de refuser une réponse mal formée.

Point d’étape — Les classes s’importent sans erreur. Si list[LigneArticle] pose problème, vérifiez que votre Python est en version 3.10 ou plus.

Étape 3 — Forcer le format JSON

On indique maintenant au modèle de répondre selon ce schéma. Deux réglages, dans l’objet de configuration : le type de réponse, fixé à JSON, et le schéma lui-même, dérivé de la classe Pydantic.

from dotenv import load_dotenv
from google import genai
from google.genai import types

load_dotenv()
client = genai.Client()

texte_facture = (
    "ALPHA INFORMATIQUE - 12/05/2026. "
    "2 claviers a 9000, 1 ecran a 65000. Total : 83000 FCFA."
)

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=f"Extrais les informations de cette facture : {texte_facture}",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_json_schema=Facture.model_json_schema(),
    ),
)
print(response.text)

Deux champs font tout le travail. response_mime_type="application/json" interdit au modèle de répondre en prose : la sortie sera du JSON et rien d’autre. response_json_schema=Facture.model_json_schema() fournit le schéma attendu, obtenu en convertissant la classe Pydantic en JSON Schema. À l’exécution, response.text contient un objet JSON propre, avec les bons champs et les bons types — pas une phrase autour.

Point d’étape — La sortie est un JSON commençant par une accolade, avec les clés commercant, date, articles, total. Si vous voyez de la prose, vérifiez que response_mime_type est bien positionné.

Étape 4 — Valider et exploiter la réponse

Le JSON est du texte ; pour le manipuler en sécurité, on le recharge dans la classe Pydantic, qui le valide au passage. Si un champ manque ou si un type ne correspond pas, Pydantic lève une erreur explicite plutôt que de laisser passer une donnée corrompue.

facture = Facture.model_validate_json(response.text)

print("Commercant :", facture.commercant)
print("Nombre de lignes :", len(facture.articles))
print("Total :", facture.total)

for ligne in facture.articles:
    print(f"  - {ligne.quantite} x {ligne.nom} a {ligne.prix_unitaire}")

Facture.model_validate_json transforme le texte JSON en un véritable objet Facture. Dès lors, on accède aux données par attribut — facture.total, facture.articles — avec l’autocomplétion de l’éditeur et la sécurité des types. On boucle sur les lignes comme sur n’importe quelle liste Python. Vous êtes passé d’une phrase ambiguë à un objet manipulable en trois lignes, sans une seule expression régulière.

Point d’étapefacture.total vaut 83000 et la boucle affiche les deux lignes. Une ValidationError signale un écart entre la réponse et le schéma : lisez le message, il pointe le champ fautif.

Étape 5 — Imbrication, listes et valeurs imposées

Les schémas réels sont rarement plats. On a déjà vu l’imbrication (des LigneArticle dans une Facture) et les listes. Ajoutons la contrainte la plus utile : restreindre un champ à un ensemble de valeurs autorisées, grâce au type Literal.

from typing import Literal
from pydantic import BaseModel

class Avis(BaseModel):
    produit: str
    note: int
    sentiment: Literal["positif", "neutre", "negatif"]
    resume: str

Le champ sentiment ne pourra valoir que l’une des trois étiquettes : le modèle est contraint de classer, pas d’inventer une nuance hors liste. C’est exactement ce qu’il faut pour de la catégorisation — trier des avis clients, router des tickets, étiqueter des messages. Cette discipline du schéma transforme un modèle « créatif » en composant prévisible, ce qui est précisément ce qu’on attend d’une brique logicielle.

On utilise ensuite ce schéma comme à l’étape 3, en passant Avis.model_json_schema(). La réponse sera un objet dont le sentiment est garanti dans la liste.

Dernier raffinement utile : tous les champs ne sont pas toujours présents dans la source. Une facture peut ne pas mentionner de numéro de TVA. Plutôt que de provoquer une erreur de validation, on déclare ces champs facultatifs avec une valeur par défaut. En Pydantic, un champ typé comme pouvant valoir None et initialisé à None devient optionnel : le modèle a le droit de l’omettre, et votre code teste simplement sa présence avant de l’utiliser. Cette souplesse est indispensable sur des documents réels, où l’information est rarement complète. On garde ainsi le meilleur des deux mondes : une structure stricte sur ce qui est obligatoire, une tolérance maîtrisée sur ce qui est accessoire. C’est exactement ce compromis qui rend un extracteur robuste face à la diversité des documents qu’on lui soumettra en production.

Étape 6 — Combiner avec le multimodal

La vraie puissance apparaît quand on additionne les capacités. Reprenez une image de facture du tutoriel multimodal et appliquez-lui le schéma de l’étape 3 : vous lisez une photo et vous en sortez un objet structuré, en un seul appel.

with open("recu.jpg", "rb") as f:
    image = f.read()

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=[
        types.Part.from_bytes(data=image, mime_type="image/jpeg"),
        "Extrais les informations de ce recu.",
    ],
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_json_schema=Facture.model_json_schema(),
    ),
)
facture = Facture.model_validate_json(response.text)
print(facture.total)

Une photo entre, un objet typé sort. C’est le cœur d’une application de numérisation de documents : plus besoin d’OCR séparé ni de parsing, le modèle fait les deux d’un coup et vous rend une donnée prête à enregistrer. On voit ici comment les tutoriels de cette série se combinent en briques.

Point d’étape — L’objet est rempli à partir de l’image. Si un champ est faux, l’image est peut-être peu lisible : le schéma garantit la forme, pas l’exactitude de lecture.

Comment Gemini garantit le format

On pourrait croire que le modèle « essaie » de produire du JSON et qu’on croise les doigts. Ce n’est pas ainsi que cela fonctionne. Quand vous fixez response_mime_type et un schéma, le modèle est guidé pendant la génération pour ne produire que des sorties compatibles avec la structure demandée : à chaque étape, les seules suites autorisées sont celles qui respectent le schéma. Le résultat est un JSON syntaxiquement valide et conforme à la forme attendue, de façon fiable, et non au petit bonheur. C’est ce qui distingue cette approche d’un simple « réponds en JSON s’il te plaît » glissé dans l’instruction : ce dernier fonctionne souvent, mais échoue parfois, et « parfois » est précisément ce qu’un programme ne tolère pas. Comprendre ce mécanisme explique pourquoi on privilégie toujours la configuration au bricolage par l’instruction.

🐞 Pièges fréquents

Symptôme Cause probable Correctif
La réponse contient de la prose autour du JSON response_mime_type non positionné. Fixer response_mime_type="application/json" dans la configuration.
ValidationError de Pydantic Un champ manque ou son type diffère du schéma. Lire le message d’erreur, ajuster le schéma ou rendre le champ optionnel.
Un champ numérique arrive en texte Ambiguïté dans la donnée source (séparateurs, devises). Préciser dans l’instruction d’ignorer la devise et les espaces.
Valeur de catégorie hors liste Champ non contraint par Literal. Restreindre le champ avec Literal[...].
JSON tronqué Réponse coupée par une limite de jetons trop basse. Augmenter max_output_tokens pour les structures longues.

Sorties structurées ou function calling ?

Les deux capacités produisent des données structurées et l’on confond parfois leurs rôles. La distinction est pourtant nette. Les sorties structurées servent quand le but final est la donnée : extraire, classer, transformer un contenu en objet exploitable. Le modèle est le point d’arrivée du flux. Le function calling, lui, sert quand la donnée structurée doit déclencher une action dans votre système — consulter un stock, créer une commande — et que le résultat de cette action revient nourrir la conversation. Le modèle est alors au milieu d’une boucle. Règle pratique : si vous voulez un objet à enregistrer, prenez les sorties structurées ; si vous voulez que le modèle agisse via votre code, prenez le function calling. Les deux se combinent d’ailleurs très bien, le function calling pouvant lui-même imposer un schéma à ses arguments.

✅ Récapitulatif

Vous avez troqué le texte libre fragile contre des données fiables. Vous savez décrire une structure avec Pydantic, forcer Gemini à la respecter via response_mime_type et response_json_schema, valider la réponse en un objet typé avec model_validate_json, et contraindre les catégories avec Literal. Surtout, vous savez combiner cette capacité avec le multimodal pour numériser un document en une seule requête. C’est le geste qui rend un modèle de langage utilisable comme un vrai composant logiciel, prévisible et testable.

🧾 Aide-mémoire

Élément Rôle
class X(BaseModel) Décrire la structure attendue.
response_mime_type="application/json" Interdire la prose, forcer du JSON.
response_json_schema=X.model_json_schema() Imposer le schéma au modèle.
X.model_validate_json(response.text) Valider et charger en objet typé.
Literal["a", "b", "c"] Restreindre un champ à des valeurs autorisées.

💪 À vous de jouer

Définissez un schéma Contact (nom, e-mail, téléphone, type parmi « client », « fournisseur », « autre ») et extrayez-le d’un court texte de présentation libre. Faites en sorte que le type soit toujours l’une des trois valeurs.

Voir une solution
from typing import Literal
from pydantic import BaseModel
from dotenv import load_dotenv
from google import genai
from google.genai import types

load_dotenv()
client = genai.Client()

class Contact(BaseModel):
    nom: str
    email: str
    telephone: str
    type: Literal["client", "fournisseur", "autre"]

texte = "Bonjour, ici Awa Ndiaye de Beta Negoce, notre fournisseur de cables. Tel 77 123 45 67, awa@beta.example."

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=f"Extrais le contact : {texte}",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_json_schema=Contact.model_json_schema(),
    ),
)
contact = Contact.model_validate_json(response.text)
print(contact.type, contact.email)

Le champ type contraint par Literal garantit une valeur exploitable pour, par exemple, router le contact vers la bonne table.

Tutoriels suivants

Pour aller plus loin

🔝 Retour au guide principal de l’API Gemini. La documentation officielle décrit les types de schéma supportés et leurs contraintes.

Questions fréquentes

Faut-il obligatoirement Pydantic ? Non : on peut passer un schéma JSON brut sous forme de dictionnaire à response_json_schema. Pydantic apporte en plus la validation et le typage côté Python, ce qui en fait l’option la plus confortable.

Le modèle peut-il quand même se tromper sur une valeur ? Le schéma garantit la forme (les bons champs, les bons types), pas l’exactitude du contenu lu. Sur une source ambiguë, vérifiez les valeurs critiques.

Peut-on imbriquer plusieurs niveaux ? Oui, autant que nécessaire : des modèles dans des modèles, des listes de modèles. Le schéma reflète n’importe quelle arborescence raisonnable.

Comment rendre un champ facultatif ? En le typant comme optionnel côté Pydantic et en lui donnant une valeur par défaut ; le modèle pourra alors l’omettre sans déclencher d’erreur de validation.

مشاركة
Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité