Aller au contenu principal

Schémas Zod en TypeScript

Zod : l’équivalent TypeScript de Pydantic

Si vous travaillez en TypeScript (ou 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.

Installer Zod et le SDK Mistral

npm install @mistralai/mistralai zod

Zod fonctionne nativement avec TypeScript et fournit une inférence de types automatique.

Définir un schéma simple

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 }

Types Zod courants

Zod supporte tous les types nécessaires pour les sorties structurées :

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>;

Correspondance Pydantic / Zod

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, vous pouvez imbriquer des schémas :

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>;

Exemple complet avec le SDK Mistral

Voici un exemple complet d’extraction structurée en TypeScript :

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();

Descriptions de champs avec .describe()

Comme Field(description=...) en Pydantic, utilisez .describe() en Zod pour guider le modèle :

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 en TypeScript

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