Aller au contenu principal
Leçon 9 sur 12 · Structured Output — JSON et Schémas

Schémas Zod en TypeScript

Leçon 9 sur 12 Mis à jour le 3 novembre 2026 Développeur
Sur cette page
  1. Zod : l’équivalent TypeScript de Pydantic
  2. Définir un schéma simple
  3. Les types dont vous aurez besoin
  4. Schémas imbriqués
  5. Un appel complet avec le SDK Mistral
  6. Décrire les champs avec .describe()
  7. Gestion des erreurs
  8. Points clés à retenir

Corrigé le 3 novembre 2026 après compilation avec le SDK @mistralai/mistralai 3.5.0, Zod 4.6 et TypeScript 7.0.2. L’exemple complet importait Mistral par défaut, alors que le SDK ne propose qu’un export nommé, et lisait dans main() une variable response qui n’existait que dans extractBook(). Les accès à parsed ne tenaient pas compte de choices et message, facultatifs dans les types du SDK, ni du type unknown de parsed, que le code renvoyait comme un Book. L’import de ZodError ne servait à rien : chat.parse() valide avec safeParse et n’en lève jamais. La clé API n’est plus écrite en dur. Sources : SDK TypeScript Mistral · exemple officiel chat.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)
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. 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, comme Field(description=...) en Python
  • Le SDK Mistral TypeScript utilise responseFormat (camelCase) au lieu de response_format
  • Une réponse non conforme laisse parsed à undefined (pas d’exception) : testez-le, puis relancez l’appel si besoin