Aller au contenu principal

Schémas Zod en TypeScript

Mis à jour le 29 juillet 2026

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)
strz.string()
int, floatz.number()
boolz.boolean()
list[str]z.array(z.string())
Optional[str]z.string().nullable()
Enumz.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. Deux points de vigilance apparaissent : le paramètre s’appelle responseFormat en camelCase, et parsed doit être testé avant usage, car le SDK le type comme potentiellement nul.

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: "votre-clé-api" });

// 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 parsed = response.choices[0].message.parsed;
  if (!parsed) {
    throw new Error("Parsed est null — réponse peut-être tronquée");
  }
  return 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}`);

  // Le JSON brut est aussi disponible
  const raw = response.choices[0].message.content;
  console.log(`JSON brut : ${raw}`);
}

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. La contrainte z.ZodType<T> fait le lien entre le schéma passé en argument et le type retourné, si bien que l’appelant récupère un objet correctement typé sans annotation supplémentaire.

import { ZodError } from "zod";

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 parsed est null");
      }
      return parsed;
    } 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, comme Field(description=...) en Python
  • Le SDK Mistral TypeScript utilise responseFormat (camelCase) au lieu de response_format
  • La gestion d’erreurs suit le même pattern qu’en Python : retry avec feedback