Zod Schemas in TypeScript
On this page
Updated November 3, 2026 after compiling with
@mistralai/mistralaiSDK 3.5.0, Zod 4.6, and TypeScript 7.0.2. The original example importedMistralby default, but the SDK only offers a named export. It also read aresponsevariable inmain()that only existed insideextractBook(). Accessingparsedignored thatchoicesandmessageare optional in the SDK types, and it treatedparsedas aBookwhen its type is actuallyunknown. TheZodErrorimport was unnecessary:chat.parse()validates withsafeParseand never throws. The API key is no longer hardcoded. Sources: Mistral TypeScript SDK · officialchat.parseexample.
Zod: The TypeScript Equivalent of Pydantic
If you work in TypeScript or JavaScript, Zod is the schema validation library used by the Mistral SDK for Custom Structured Outputs. It plays exactly the same role as Pydantic in Python: defining a strict schema that the model must follow. Everything you learned about schemas applies here, with only syntax differences.
Install both the SDK and Zod in a single command:
npm install @mistralai/mistralai zod
Zod works natively with TypeScript and provides automatic type inference—its main advantage over writing interfaces by hand.
Defining a Simple Schema
Build a Zod schema with z.object(), assigning a validator to each field. The z.infer line makes the difference: it deduces the TypeScript type from the schema, so you don’t have to maintain two parallel declarations that could drift apart.
import { z } from "zod";
const ContactSchema = z.object({
name: z.string(),
email: z.string(),
phone: z.string(),
company: z.string(),
});
// TypeScript automatically infers the type
type Contact = z.infer<typeof ContactSchema>;
// => { name: string; email: string; phone: string; company: string }
Types You Will Need
Zod covers every need for structured outputs. The ticket schema below brings together common cases: basic fields, a nullable field for an unassigned ticket, an enum for priority, and typed arrays.
import { z } from "zod";
const TicketSchema = z.object({
// Basic types
title: z.string(),
description: z.string(),
ticketId: z.number(),
confidence: z.number(),
isUrgent: z.boolean(),
// Optional (can be null)
assignee: z.string().nullable(),
// Enum (constrained values)
priority: z.enum(["low", "medium", "high", "critical"]),
// Lists
tags: z.array(z.string()),
relatedIds: z.array(z.number()),
});
type Ticket = z.infer<typeof TicketSchema>;
One nuance deserves attention: where Python distinguishes int and float, Zod knows only z.number(). Keep this mapping in mind when porting a schema from one language to the other.
| 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([...]) |
Nested Schemas
As with Pydantic, a schema can contain another: just reference the constant. Composition then reads from the innermost to the outermost: the address inside the company, the company inside the person.
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>;
A Complete Call with the Mistral SDK
Here is the same book extraction from the Python lesson, ported to TypeScript. Three points need attention. The parameter is responseFormat in camelCase. You must test parsed before using it: the SDK validates the response against the schema with safeParse and, if the JSON is truncated or does not match the schema, leaves parsed as undefined instead of throwing an error. Finally, the SDK does not carry your schema’s type: it declares parsed as unknown. Passing the object through BookSchema.parse() makes it typed as Book and revalidates it in the process.
import { Mistral } from "@mistralai/mistralai";
import { z } from "zod";
// 1. Define the schema
const BookSchema = z.object({
title: z.string().describe("Book title"),
authors: z.array(z.string()).describe("List of authors"),
year: z.number().nullable().describe("Publication year"),
genre: z.string().describe("Main literary genre"),
summary: z.string().describe("Summary in 2-3 sentences"),
});
type Book = z.infer<typeof BookSchema>;
// 2. Create the client
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
// 3. Call chat.parse()
async function extractBook(text: string): Promise<Book> {
const response = await client.chat.parse({
model: "mistral-large-latest",
messages: [
{
role: "system",
content: "You are an expert librarian. Extract book information.",
},
{ role: "user", content: text },
],
responseFormat: BookSchema,
temperature: 0,
});
// 4. Access the parsed object
const message = response.choices?.[0]?.message;
if (!message?.parsed) {
throw new Error("Response missing, truncated, or non-compliant with schema");
}
// Raw JSON remains available in message.content
console.log(`Raw JSON: ${message.content}`);
// parsed is typed as unknown: BookSchema.parse() returns a typed Book
return BookSchema.parse(message.parsed);
}
// 5. Usage
async function main() {
const book = await extractBook(
"I loved '1984' by George Orwell, published in 1949. A dystopian classic."
);
console.log(`Title: ${book.title}`);
console.log(`Authors: ${book.authors.join(", ")}`);
console.log(`Year: ${book.year}`);
console.log(`Genre: ${book.genre}`);
}
main();
Describing Fields with .describe()
In Zod, .describe() fills the same role as Field(description=...) in Pydantic: telling the model what the field should contain, especially when the name alone is ambiguous. A score without a description might land on a 0–100 scale; with an explicit range mentioned, it follows your convention.
const SentimentSchema = z.object({
sentiment: z.enum(["positive", "negative", "neutral"]).describe(
"Overall sentiment of the text"
),
score: z.number().describe(
"Confidence score between 0.0 and 1.0"
),
keywords: z.array(z.string()).describe(
"The 3–5 main keywords"
),
summary: z.string().describe(
"Summary in one sentence maximum"
),
});
Error Handling
The retry pattern follows the same logic as in Python, with a generic signature that makes it reusable for any schema. There is no need to catch a ZodError: chat.parse() does not throw one; it leaves parsed empty. That case, along with network or HTTP errors, triggers a retry. The constraint z.ZodType<T> links the schema passed as an argument to the return type: schema.parse() returns a T, so the caller gets a properly typed object without extra annotations.
// Continuation of the same file: client, z, and BookSchema are already defined
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: "Extract the requested information." },
{ role: "user", content: text },
],
responseFormat: schema,
temperature: 0,
});
const parsed = response.choices?.[0]?.message?.parsed;
if (!parsed) {
throw new Error("Response missing or non-compliant with schema");
}
return schema.parse(parsed); // restores type T
} catch (error) {
console.error(`Attempt ${attempt + 1} failed:`, error);
if (attempt === maxRetries) throw error;
}
}
throw new Error("Unexpected failure");
}
// Usage
try {
const book = await safeExtract("My text here", BookSchema);
console.log(book.title);
} catch (error) {
console.error("Extraction failed:", error);
}
Key Takeaways
- Zod is the TypeScript equivalent of Pydantic for Custom Structured Outputs
z.infer<typeof Schema>automatically generates the TypeScript type.describe()adds per-field instructions, likeField(description=...)in Python- The Mistral TypeScript SDK uses
responseFormat(camelCase) instead ofresponse_format - A non-compliant response leaves
parsedasundefined(no exception): test for it, then retry if needed