Aller au contenu
Développement medium

Sorties structurées avec Ollama : JSON garanti et Pydantic en local

15 min de lecture

Vous demandez à un LLM de remplir un formulaire en JSON et une fois sur cinq il vous renvoie du texte introductif comme « Voici le JSON demandé : » avant le bloc, ou il invente un guillemet en trop, ou il oublie une virgule. Votre pipeline casse. Les sorties structurées d'Ollama résolvent ce problème en 2026 : on impose au modèle un format JSON strict via un schéma, et la réponse est garantie syntaxiquement valide.

  • Ce qu'est une « sortie structurée » et pourquoi elle change la vie d'un développeur qui consomme un LLM en local
  • Comment forcer Ollama à respecter un JSON Schema brut (l'approche minimaliste)
  • Comment utiliser Pydantic pour avoir un objet Python typé directement, sans parser
  • Là où le modèle hallucine malgré le schéma — un piège que personne ne documente

Avant 2024, pour obtenir du JSON d'un LLM, vous deviez :

  1. Écrire un prompt qui demandait gentiment « renvoie uniquement du JSON sans texte autour »
  2. Croiser les doigts
  3. Encadrer votre json.loads() d'un try/except parce qu'une fois sur cinq, ça pétait

Depuis Ollama 0.5 (et amélioré dans la 0.24 actuelle), le paramètre format vous permet de passer un schéma JSON au modèle. Côté serveur, Ollama force la génération token par token à respecter ce schéma. Résultat : le JSON est valide à 100 %, même sur des petits modèles 7 B.

Concrètement, les sorties structurées vous permettent de :

  • Extraire des entités d'un texte libre (email, ticket, CV) → JSON typé
  • Classifier une entrée dans une catégorie d'une liste prédéfinie (énumération)
  • Faire des agents locaux qui produisent des structures de données consommées par votre code
  • Analyser des images et obtenir une description structurée (cf. guide Vision)

Pour suivre ce guide, vous avez besoin :

  1. Un Ollama qui tourne en local avec un modèle qui supporte les sorties structurées. Les modèles validés en 2026 :

    Fenêtre de terminal
    ollama pull qwen3 # 5.2 Go, le plus stable
    # ou
    ollama pull gemma4 # 9.6 Go, vision + tools natifs
    # ou
    ollama pull llama3.1 # 4.7 Go, valeur sûre

    Si vous n'avez pas encore Ollama installé, suivez le guide d'installation Ollama d'abord.

  2. Un projet Python avec les dépendances suivantes (épinglées pour la reproductibilité) :

    pyproject.toml
    [project]
    name = "lab-ollama-structured"
    requires-python = ">=3.10"
    dependencies = [
    "ollama==0.6.2",
    "pydantic[email]==2.13.4",
    ]
    Fenêtre de terminal
    uv sync
    # ou : pip install "ollama==0.6.2" "pydantic[email]==2.13.4"
  3. Vérifier qu'Ollama écoute sur le port par défaut :

    Fenêtre de terminal
    curl http://localhost:11434/api/tags

    Vous devez voir la liste des modèles installés.

C'est la méthode la plus directe : on écrit le schéma JSON à la main et on le passe au paramètre format du client Ollama.

Cas d'usage : on reçoit un email de support et on veut extraire les informations clés en JSON pour les router vers la bonne équipe.

01-json-schema.py
import json
from ollama import Client
MODEL = "qwen3"
HOST = "http://localhost:11434"
EMAIL = """
De: marie.dupont@acme-corp.fr
Objet: Problème avec l'API key
Bonjour, mes appels API retournent 401 Unauthorized depuis ce matin.
Numéro client : ACME-4271. Plan Pro, carte à jour.
Marie Dupont, Lead Developer chez ACME Corp
"""
# Le schéma JSON : la structure que le modèle DOIT respecter
SCHEMA = {
"type": "object",
"properties": {
"expediteur_nom": {"type": "string"},
"expediteur_email": {"type": "string", "format": "email"},
"entreprise": {"type": "string"},
"numero_client": {"type": "string"},
"categorie": {
"type": "string",
"enum": ["authentification", "facturation", "performance", "bug", "autre"],
},
"priorite": {"type": "string", "enum": ["basse", "normale", "haute", "critique"]},
"resume": {"type": "string"},
},
"required": ["expediteur_nom", "expediteur_email", "entreprise", "categorie", "priorite", "resume"],
}
client = Client(host=HOST)
response = client.chat(
model=MODEL,
messages=[
{"role": "system", "content": "Tu extrais les informations d'un email de support en JSON."},
{"role": "user", "content": EMAIL},
],
format=SCHEMA, # 👈 ICI la magie
options={"temperature": 0},
)
raw = response["message"]["content"]
print(raw)
# Le json.loads() ci-dessous ne peut PAS échouer : Ollama garantit la validité.
parsed = json.loads(raw)
print(parsed["categorie"], "", parsed["priorite"])

Sortie réelle (validée sur H100 avec qwen3) :

{
"expediteur_nom": "Marie Dupont",
"expediteur_email": "marie.dupont@acme-corp.fr",
"entreprise": "ACME Corp",
"numero_client": "ACME-4271",
"categorie": "authentification",
"priorite": "haute",
"resume": "Problème avec l'API key, appels API retournent 401 Unauthorized"
}

Méthode 2 : Pydantic (la plus pratique au quotidien)

Section intitulée « Méthode 2 : Pydantic (la plus pratique au quotidien) »

Écrire un schéma JSON à la main pour chaque sortie est vite pénible et sujet aux fautes de frappe. Pydantic est une bibliothèque Python qui vous permet de décrire votre structure comme une classe Python : Pydantic génère le schéma JSON automatiquement, valide la réponse du LLM, et vous retourne un objet Python avec l'autocomplétion dans votre IDE.

Pydantic (de l'anglais pydantic, contraction de « Python » + « pedantic ») est la lib de validation de données la plus utilisée en Python en 2026. Elle équipe FastAPI, OpenAI SDK, et la plupart des frameworks IA.

02-pydantic.py
from typing import Literal
from ollama import Client
from pydantic import BaseModel, EmailStr, Field
MODEL = "qwen3"
HOST = "http://localhost:11434"
class TicketSupport(BaseModel):
"""Ticket extrait d'un email entrant."""
expediteur_nom: str
expediteur_email: EmailStr
expediteur_role: str | None = None
entreprise: str
numero_client: str | None = None
categorie: Literal["authentification", "facturation", "performance", "bug", "autre"]
priorite: Literal["basse", "normale", "haute", "critique"]
resume: str = Field(description="Synthèse en 1-2 phrases du problème exposé.")
EMAIL = """
De: paul.martin@datalab.io
Objet: Latence très élevée depuis hier
Vos endpoints /v1/embeddings prennent 8-12 secondes depuis hier soir.
Numéro client : DTL-998. — Paul, CTO Datalab
"""
client = Client(host=HOST)
response = client.chat(
model=MODEL,
messages=[{"role": "user", "content": EMAIL}],
# Pydantic génère le JSON Schema automatiquement
format=TicketSupport.model_json_schema(),
options={"temperature": 0},
)
# Validation + objet typé en une ligne
ticket = TicketSupport.model_validate_json(response["message"]["content"])
print(ticket.categorie) # autocomplétion IDE
print(ticket.priorite)

Sortie réelle :

performance
haute

L'un des cas d'usage les plus rentables est la classification. Un LLM est très bon pour catégoriser un texte, mais il a tendance à inventer de nouvelles catégories ou à formuler à sa façon. Avec Literal[...] (Pydantic) ou enum (JSON Schema), il est forcé de choisir dans votre liste.

from typing import Literal
from pydantic import BaseModel
class Classification(BaseModel):
sentiment: Literal["positif", "neutre", "negatif"]
urgence: Literal["faible", "moyenne", "haute"]
departement_concerne: Literal["support", "ventes", "tech", "rh", "autre"]

C'est la garantie que votre code aval n'aura jamais à gérer une catégorie inattendue.

Le piège : le schéma garantit la structure, pas la véracité

Section intitulée « Le piège : le schéma garantit la structure, pas la véracité »

Voici le piège que personne ne mentionne dans les tutos. Regardez ce code :

03-limits.py
class Personne(BaseModel):
nom: str
email: EmailStr
age: int
pays: Literal["France", "Belgique", "Suisse", "Canada", "Autre"]
# Prompt pauvre : on demande des infos non fournies
response = client.chat(
model="qwen3",
messages=[{"role": "user", "content": "Donne-moi les informations d'un utilisateur."}],
format=Personne.model_json_schema(),
)
print(response["message"]["content"])

Sortie réelle obtenue sur H100 :

{"nom": "Utilisateur", "email": "utilisateur@example.com", "age": 25, "pays": "France"}

Le JSON est syntaxiquement valide. Pydantic ne lèvera aucune exception. Mais ces données sont purement inventées : le LLM a halluciné un utilisateur générique pour respecter le schéma. C'est un problème de fond pour les applications de production.

Mesures réelles sur NVIDIA H100 PCIe 80 Go avec Ollama 0.24 :

ModèleVRAMTokens/sFiabilité schéma
qwen35.2 Go~160Excellente
gemma49.6 Go~120Excellente
llama3.14.7 Go~140Bonne
mistral4.4 Go~100Moyenne (parfois inventif)

Mon choix par défaut en 2026 : qwen3 — le meilleur compromis taille / vitesse / fidélité au schéma.

Q : Mon modèle ignore le format et me renvoie du texte libre. R : Vérifiez que vous utilisez Ollama serveur 0.5 minimum (ollama --version). Sur les versions plus anciennes, seul le mode format="json" (sans schéma) existe. Mettez à jour avec curl -fsSL https://ollama.com/install.sh | sh.

Q : model_validate_json lève une ValidationError sur un email non conforme. R : Bon signe — Pydantic vous protège d'un email mal formé. Le LLM en a produit un techniquement valide selon email-validator, mais qui ne correspond pas à votre contexte. Loggez l'erreur et faites un fallback (champ à None, retry avec un prompt plus précis).

Q : Peut-on utiliser des schémas imbriqués (objets dans objets) ? R : Oui, Pydantic gère parfaitement les modèles imbriqués. Définissez vos classes l'une dans l'autre et model_json_schema() produira un schéma JSON avec $defs/$ref. Ollama suit.

Q : Quelle différence entre format="json" (legacy) et format=schema ? R : format="json" (chaîne magique) force uniquement du JSON valide, sans contrainte de structure → le modèle peut inventer les clés. format=<dict de schéma> force la structure exacte. Toujours préférer le schéma.

Q : Le modèle remplit-il des champs nullable quand l'info manque ? R : Variable. Avec qwen3 et un prompt explicite (« mets à null si l'information n'est pas dans le texte »), oui ~90 % du temps. Sans instruction explicite, il préfère souvent inventer.

  • Les sorties structurées rendent le JSON garanti — fini les try/except autour de json.loads().
  • JSON Schema brut pour des cas simples ; Pydantic dès qu'il y a plus de 3 champs ou de la validation métier.
  • Literal (Pydantic) ou enum (JSON Schema) pour la classification : interdit au modèle d'inventer une catégorie.
  • Le schéma garantit la structure, pas la véracité : combinez avec un contexte riche et autorisez None explicitement.
  • qwen3 et gemma4 sont les modèles les plus fiables sur ce cas d'usage en 2026.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn