Validation avec JSON Schema et Zod
Validation avec JSON Schema et Zod
Le Structured Output garantit la structure de la réponse, mais pas la validité sémantique des données. Un champ “email” peut contenir une chaîne qui n’est pas un email valide. La validation côté client avec JSON Schema ou Zod est le complément indispensable pour une pipeline robuste.
Pourquoi valider après réception
Le Structured Output d’OpenAI garantit :
- Le JSON est syntaxiquement valide
- Les types sont corrects (string, number, etc.)
- Les champs requis sont présents
- Les enums sont respectés
Ce qu’il ne garantit pas :
- Les contraintes de format (email, URL, date ISO)
- Les contraintes de longueur (min/max)
- Les contraintes numériques (min/max, multiples)
- La cohérence entre les champs (date_fin > date_debut)
- La validité métier (SIRET à 14 chiffres)
Validation Python avec jsonschema
import json
from jsonschema import validate, ValidationError
# Schéma pour l'API OpenAI (strict, sans format/pattern)
api_schema = {
"type": "object",
"properties": {
"email": {"type": "string"},
"age": {"type": "integer"},
"role": {"type": "string", "enum": ["admin", "user", "viewer"]}
},
"required": ["email", "age", "role"],
"additionalProperties": False
}
# Schéma étendu pour la validation client
validation_schema = {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"pattern": r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$"
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"role": {"type": "string", "enum": ["admin", "user", "viewer"]}
},
"required": ["email", "age", "role"]
}
def valider_reponse(data: dict) -> tuple[bool, str]:
"""Valide la réponse du LLM avec le schéma étendu."""
try:
validate(instance=data, schema=validation_schema)
return True, "Valide"
except ValidationError as e:
return False, f"Erreur : {e.message}"
Validation avec Pydantic
Pydantic est souvent plus pratique que jsonschema pour la validation en Python :
from pydantic import BaseModel, Field, field_validator
from pydantic import EmailStr
from datetime import date
class ContactExtraction(BaseModel):
"""Schéma de validation pour l'extraction de contacts."""
nom: str = Field(min_length=2, max_length=100)
prenom: str = Field(min_length=2, max_length=100)
email: EmailStr
telephone: str = Field(pattern=r"^\+?[0-9\s\-\.]{10,15}$")
entreprise: str
poste: str
date_extraction: str
@field_validator("date_extraction")
@classmethod
def validate_date(cls, v):
try:
date.fromisoformat(v)
return v
except ValueError:
raise ValueError(f"Date invalide : {v}")
def extraire_et_valider(texte: str) -> ContactExtraction:
"""Extrait un contact et valide les données."""
# Schéma pour l'API (sans les contraintes fines)
api_schema = {
"type": "object",
"properties": {
"nom": {"type": "string"},
"prenom": {"type": "string"},
"email": {"type": "string"},
"telephone": {"type": "string"},
"entreprise": {"type": "string"},
"poste": {"type": "string"},
"date_extraction": {"type": "string"}
},
"required": ["nom", "prenom", "email", "telephone",
"entreprise", "poste", "date_extraction"],
"additionalProperties": False
}
response = client.responses.create(
model="gpt-5.3",
instructions="Extrais les informations de contact. "
"Format date : YYYY-MM-DD.",
input=texte,
text={
"format": {
"type": "json_schema",
"name": "contact",
"schema": api_schema,
"strict": True
}
}
)
data = json.loads(response.output_text)
# Validation Pydantic avec contraintes fines
return ContactExtraction(**data)
Validation avec Zod (TypeScript/Node.js)
Si votre backend est en TypeScript, Zod est l’outil de référence :
import { z } from "zod";
import OpenAI from "openai";
const client = new OpenAI();
// Schéma Zod avec validations fines
const InvoiceSchema = z.object({
numero: z.string().regex(/^[A-Z]{2}-\d{4}-\d{4}$/),
date: z.string().date(),
montant_ht: z.number().positive(),
tva_taux: z.number().min(0).max(100),
montant_ttc: z.number().positive(),
fournisseur: z.object({
nom: z.string().min(2),
siret: z.string().length(14).regex(/^\d+$/),
}),
});
type Invoice = z.infer<typeof InvoiceSchema>;
async function extractInvoice(text: string): Promise<Invoice> {
const response = await client.responses.create({
model: "gpt-5.3",
instructions: "Extrais les données de facture.",
input: text,
text: {
format: {
type: "json_schema",
name: "invoice",
schema: {
type: "object",
properties: {
numero: { type: "string" },
date: { type: "string" },
montant_ht: { type: "number" },
tva_taux: { type: "number" },
montant_ttc: { type: "number" },
fournisseur: {
type: "object",
properties: {
nom: { type: "string" },
siret: { type: "string" },
},
required: ["nom", "siret"],
additionalProperties: false,
},
},
required: ["numero", "date", "montant_ht",
"tva_taux", "montant_ttc", "fournisseur"],
additionalProperties: false,
},
strict: true,
},
},
});
const data = JSON.parse(response.output_text);
// Validation Zod avec contraintes fines
return InvoiceSchema.parse(data);
}
Pattern : retry avec correction
Quand la validation échoue, renvoyez l’erreur au modèle pour qu’il corrige :
def extract_with_retry(prompt: str, schema: dict,
validator, max_retries: int = 3) -> dict:
"""Extrait et valide avec retry automatique."""
last_error = None
for attempt in range(max_retries):
input_text = prompt
if last_error:
input_text += (f"\n\nATTENTION : ta réponse précédente avait "
f"cette erreur de validation : {last_error}\n"
f"Corrige et réessaie.")
response = client.responses.create(
model="gpt-5.3",
input=input_text,
text={"format": {"type": "json_schema",
"name": "data",
"schema": schema,
"strict": True}},
temperature=0.1
)
data = json.loads(response.output_text)
is_valid, error = validator(data)
if is_valid:
return data
last_error = error
raise ValueError(f"Échec après {max_retries} tentatives : {last_error}")
Mise en pratique
- Créez un schéma Pydantic pour extraire des événements d’agenda (titre, date, heure, lieu, participants)
- Implémentez l’extraction avec Structured Output + validation Pydantic
- Testez avec 5 textes d’emails contenant des invitations
- Implémentez le retry automatique pour les cas où la validation échoue
- Mesurez le taux de succès au premier essai vs après retry
Points clés à retenir
- Le Structured Output garantit la structure, pas la validité sémantique
- Utilisez deux schémas : un pour l’API (strict), un pour la validation client
- Pydantic (Python) et Zod (TypeScript) sont les outils de référence
- Le pattern retry avec feedback corrige la plupart des erreurs de validation
- Validez toujours côté client avant de persister les données