Schémas Zod en TypeScript
Sur cette page
Corrigé le 3 novembre 2026 après compilation avec le SDK
@mistralai/mistralai3.5.0, Zod 4.6 et TypeScript 7.0.2. L’exemple complet importaitMistralpar défaut, alors que le SDK ne propose qu’un export nommé, et lisait dansmain()une variableresponsequi n’existait que dansextractBook(). Les accès àparsedne tenaient pas compte dechoicesetmessage, facultatifs dans les types du SDK, ni du typeunknowndeparsed, que le code renvoyait comme unBook. L’import deZodErrorne servait à rien :chat.parse()valide avecsafeParseet n’en lève jamais. La clé API n’est plus écrite en dur. Sources : SDK TypeScript Mistral · exemple officielchat.parse.
Zod : l’équivalent TypeScript de Pydantic
Si vous travaillez en TypeScript ou en JavaScript, Zod est la bibliothèque de validation de schémas utilisée par le SDK Mistral pour les Custom Structured Outputs. Elle joue exactement le même rôle que Pydantic en Python : définir un schéma strict que le modèle doit respecter. Tout ce que vous avez appris sur les schémas se transpose donc, à la syntaxe près.
L’installation réunit le SDK et Zod en une commande :
npm install @mistralai/mistralai zod
Zod fonctionne nativement avec TypeScript et fournit une inférence de types automatique — c’est son principal avantage sur une simple interface écrite à la main.
Définir un schéma simple
Un schéma Zod se construit avec z.object(), chaque champ recevant son validateur. La ligne z.infer est celle qui fait la différence : elle déduit le type TypeScript du schéma, ce qui vous évite de maintenir deux déclarations parallèles qui finiraient par diverger.
import { z } from "zod";
const ContactSchema = z.object({
name: z.string(),
email: z.string(),
phone: z.string(),
company: z.string(),
});
// TypeScript infère automatiquement le type
type Contact = z.infer<typeof ContactSchema>;
// => { name: string; email: string; phone: string; company: string }
Les types dont vous aurez besoin
Zod couvre tous les besoins des sorties structurées. Le schéma de ticket ci-dessous rassemble les cas courants : champs de base, champ nullable pour un assigné non défini, énumération pour la priorité, et tableaux typés.
import { z } from "zod";
const TicketSchema = z.object({
// Types de base
title: z.string(),
description: z.string(),
ticketId: z.number(),
confidence: z.number(),
isUrgent: z.boolean(),
// Optionnel (peut être null)
assignee: z.string().nullable(),
// Enum (valeurs contraintes)
priority: z.enum(["low", "medium", "high", "critical"]),
// Listes
tags: z.array(z.string()),
relatedIds: z.array(z.number()),
});
type Ticket = z.infer<typeof TicketSchema>;
Une nuance mérite attention : là où Python distingue int et float, Zod ne connaît que z.number(). Si vous portez un schéma d’un langage à l’autre, gardez cette correspondance sous les yeux.
| Pydantic (Python) | Zod (TypeScript) |
|---|---|
str | z.string() |
int, float | z.number() |
bool | z.boolean() |
list[str] | z.array(z.string()) |
Optional[str] | z.string().nullable() |
Enum | z.enum([...]) |
Schémas imbriqués
Comme avec Pydantic, un schéma peut en contenir un autre : il suffit de référencer la constante. La composition se lit alors du plus interne au plus externe, l’adresse dans l’entreprise, l’entreprise dans la personne.
import { z } from "zod";
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
postalCode: z.string(),
country: z.string(),
});
const CompanySchema = z.object({
name: z.string(),
industry: z.string(),
address: AddressSchema,
});
const PersonSchema = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string(),
role: z.string(),
company: CompanySchema,
});
type Person = z.infer<typeof PersonSchema>;
Un appel complet avec le SDK Mistral
Voici la même extraction de livre que dans la leçon Python, transposée en TypeScript. Trois points de vigilance apparaissent. Le paramètre s’appelle responseFormat en camelCase. parsed doit être testé avant usage : le SDK valide la réponse contre le schéma avec safeParse et, si le JSON est tronqué ou ne respecte pas le schéma, laisse parsed à undefined au lieu de lever une erreur. Enfin, le SDK ne transmet pas le type de votre schéma : il déclare parsed comme unknown. Repasser l’objet dans BookSchema.parse() le rend typé Book, et le revalide au passage.
import { Mistral } from "@mistralai/mistralai";
import { z } from "zod";
// 1. Définir le schéma
const BookSchema = z.object({
title: z.string().describe("Titre du livre"),
authors: z.array(z.string()).describe("Liste des auteurs"),
year: z.number().nullable().describe("Année de publication"),
genre: z.string().describe("Genre littéraire principal"),
summary: z.string().describe("Résumé en 2-3 phrases"),
});
type Book = z.infer<typeof BookSchema>;
// 2. Créer le client
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
// 3. Appeler chat.parse()
async function extractBook(text: string): Promise<Book> {
const response = await client.chat.parse({
model: "mistral-large-latest",
messages: [
{
role: "system",
content: "Tu es un bibliothécaire expert. Extrais les informations du livre.",
},
{ role: "user", content: text },
],
responseFormat: BookSchema,
temperature: 0,
});
// 4. Accéder à l'objet parsé
const message = response.choices?.[0]?.message;
if (!message?.parsed) {
throw new Error("Réponse absente, tronquée ou non conforme au schéma");
}
// Le JSON brut reste disponible dans message.content
console.log(`JSON brut : ${message.content}`);
// parsed est typé unknown : BookSchema.parse() renvoie un Book typé
return BookSchema.parse(message.parsed);
}
// 5. Utilisation
async function main() {
const book = await extractBook(
"J'ai adoré '1984' de George Orwell, publié en 1949. Un classique dystopique."
);
console.log(`Titre : ${book.title}`);
console.log(`Auteurs : ${book.authors.join(", ")}`);
console.log(`Année : ${book.year}`);
console.log(`Genre : ${book.genre}`);
}
main();
Décrire les champs avec .describe()
.describe() remplit en Zod le rôle de Field(description=...) en Pydantic : préciser au modèle ce que le champ doit contenir, en particulier quand le nom seul est ambigu. Un score sans description peut arriver sur une échelle de 0 à 100 ; avec la mention explicite de l’intervalle, il respecte votre convention.
const SentimentSchema = z.object({
sentiment: z.enum(["positive", "negative", "neutral"]).describe(
"Le sentiment global du texte"
),
score: z.number().describe(
"Score de confiance entre 0.0 et 1.0"
),
keywords: z.array(z.string()).describe(
"Les 3-5 mots-clés principaux"
),
summary: z.string().describe(
"Résumé en une phrase maximum"
),
});
Gestion des erreurs
Le pattern de retry suit la même logique qu’en Python, avec une signature générique qui le rend réutilisable pour n’importe quel schéma. Inutile d’attraper une ZodError : chat.parse() n’en lève pas, il laisse parsed vide ; c’est ce cas, comme les erreurs réseau ou HTTP, qui déclenche une nouvelle tentative. La contrainte z.ZodType<T> fait le lien entre le schéma passé en argument et le type retourné : schema.parse() renvoie un T, si bien que l’appelant récupère un objet correctement typé sans annotation supplémentaire.
// Suite du même fichier : client, z et BookSchema sont déjà définis
async function safeExtract<T>(
text: string,
schema: z.ZodType<T>,
maxRetries = 2
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await client.chat.parse({
model: "mistral-large-latest",
messages: [
{ role: "system", content: "Extrais les informations demandées." },
{ role: "user", content: text },
],
responseFormat: schema,
temperature: 0,
});
const parsed = response.choices?.[0]?.message?.parsed;
if (!parsed) {
throw new Error("Réponse absente ou non conforme au schéma");
}
return schema.parse(parsed); // redonne le type T
} catch (error) {
console.error(`Tentative ${attempt + 1} échouée :`, error);
if (attempt === maxRetries) throw error;
}
}
throw new Error("Échec inattendu");
}
// Utilisation
try {
const book = await safeExtract("Mon texte ici", BookSchema);
console.log(book.title);
} catch (error) {
console.error("Extraction échouée :", error);
}
Points clés à retenir
- Zod est l’équivalent TypeScript de Pydantic pour les Custom Structured Outputs
z.infer<typeof Schema>génère automatiquement le type TypeScript.describe()ajoute des instructions par champ, commeField(description=...)en Python- Le SDK Mistral TypeScript utilise
responseFormat(camelCase) au lieu deresponse_format - Une réponse non conforme laisse
parsedàundefined(pas d’exception) : testez-le, puis relancez l’appel si besoin