Zum Hauptinhalt springen
Lektion 9 von 12 · Strukturierte Ausgabe: JSON und Schemata

Zod-Schemata in TypeScript

Lektion 9 von 12 Aktualisiert am 3. November 2026 Entwickler
Auf dieser Seite
  1. Zod: Das TypeScript-Äquivalent von Pydantic
  2. Definition eines einfachen Schemas
  3. Typen, die Sie benötigen
  4. Verschachtelte Schemata
  5. Ein vollständiger Aufruf mit dem Mistral SDK
  6. Felder mit .describe() beschreiben
  7. Fehlerbehandlung
  8. Kernaussagen

Aktualisiert am 3. November 2026 nach der Kompilierung mit dem SDK @mistralai/mistralai 3.5.0, Zod 4.6 und TypeScript 7.0.2. Das ursprüngliche Beispiel importierte standardmäßig Mistral, das SDK bietet jedoch nur einen benannten Export. Zudem wurde in main() auf die Variable response zugegriffen, die nur innerhalb von extractBook() existierte. Der Zugriff auf parsed ignorierte, dass choices und message in den SDK-Typen optional sind, und behandelte parsed als Book, obwohl sein Typ eigentlich unknown ist. Der Import von ZodError war unnötig: chat.parse() validiert mit safeParse und wirft nie eine Ausnahme. Der API-Schlüssel ist nicht mehr hartcodiert. Quellen: Mistral TypeScript SDK · offizielles chat.parse-Beispiel.

Zod: Das TypeScript-Äquivalent von Pydantic

Wenn Sie in TypeScript oder JavaScript arbeiten, ist Zod die Schema-Validierungsbibliothek, die vom Mistral SDK für Custom Structured Outputs verwendet wird. Sie erfüllt exakt dieselbe Rolle wie Pydantic in Python: Sie definiert ein striktes Schema, das das Modell einhalten muss. Alles, was Sie über Schemata gelernt haben, gilt hier ebenfalls, lediglich die Syntax unterscheidet sich.

Installieren Sie sowohl das SDK als auch Zod mit einem einzigen Befehl:

npm install @mistralai/mistralai zod

Zod funktioniert nativ mit TypeScript und bietet automatische Typableitung – sein Hauptvorteil gegenüber dem manuellen Schreiben von Schnittstellen.

Definition eines einfachen Schemas

Erstellen Sie ein Zod-Schema mit z.object() und weisen Sie jedem Feld einen Validierer zu. Die Zeile z.infer macht den Unterschied: Sie leitet den TypeScript-Typ aus dem Schema ab, sodass Sie keine zwei parallelen Deklarationen pflegen müssen, die auseinanderdriften könnten.

import { z } from "zod";

const ContactSchema = z.object({
  name: z.string(),
  email: z.string(),
  phone: z.string(),
  company: z.string(),
});

// TypeScript leitet den Typ automatisch ab
type Contact = z.infer<typeof ContactSchema>;
// => { name: string; email: string; phone: string; company: string }

Typen, die Sie benötigen

Zod deckt alle Anforderungen für strukturierte Ausgaben ab. Das Ticket-Schema unten fasst gängige Fälle zusammen: Basisfelder, ein nullable-Feld für ein unzuweisbares Ticket, ein Enum für die Priorität und typisierte Arrays.

import { z } from "zod";

const TicketSchema = z.object({
  // Basistypen
  title: z.string(),
  description: z.string(),
  ticketId: z.number(),
  confidence: z.number(),
  isUrgent: z.boolean(),

  // Optional (kann null sein)
  assignee: z.string().nullable(),

  // Enum (eingeschränkte Werte)
  priority: z.enum(["low", "medium", "high", "critical"]),

  // Listen
  tags: z.array(z.string()),
  relatedIds: z.array(z.number()),
});

type Ticket = z.infer<typeof TicketSchema>;

Ein Nuance verdient Aufmerksamkeit: Während Python int und float unterscheidet, kennt Zod nur z.number(). Halten Sie diese Zuordnung im Kopf, wenn Sie ein Schema von einer Sprache in die andere portieren.

Pydantic (Python)Zod (TypeScript)
strz.string()
int, floatz.number()
boolz.boolean()
list[str]z.array(z.string())
Optional[str]z.string().nullable()
Enumz.enum([...])

Verschachtelte Schemata

Wie bei Pydantic kann ein Schema ein anderes enthalten: Verweisen Sie einfach auf die Konstante. Die Komposition wird dann von innen nach außen gelesen: Die Adresse innerhalb des Unternehmens, das Unternehmen innerhalb der 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>;

Ein vollständiger Aufruf mit dem Mistral SDK

Hier ist dieselbe Buchextraktion aus der Python-Lektion, portiert nach TypeScript. Drei Punkte verdienen Aufmerksamkeit. Der Parameter heißt responseFormat in camelCase. Sie müssen parsed vor der Verwendung testen: Das SDK validiert die Antwort gegen das Schema mit safeParse und lässt parsed als undefined zurück, wenn das JSON abgetrennt wurde oder dem Schema nicht entspricht, anstatt eine Ausnahme zu werfen. Schließlich trägt das SDK den Typ Ihres Schemas nicht mit sich: Es deklariert parsed als unknown. Das Übergeben des Objekts durch BookSchema.parse() macht es als Book typisiert und validiert es erneut im Prozess.

import { Mistral } from "@mistralai/mistralai";
import { z } from "zod";

// 1. Schema definieren
const BookSchema = z.object({
  title: z.string().describe("Titel des Buches"),
  authors: z.array(z.string()).describe("Liste der Autoren"),
  year: z.number().nullable().describe("Erscheinungsjahr"),
  genre: z.string().describe("Hauptliterarische Gattung"),
  summary: z.string().describe("Zusammenfassung in 2–3 Sätzen"),
});

type Book = z.infer<typeof BookSchema>;

// 2. Client erstellen
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });

// 3. chat.parse() aufrufen
async function extractBook(text: string): Promise<Book> {
  const response = await client.chat.parse({
    model: "mistral-large-latest",
    messages: [
      {
        role: "system",
        content: "Sie sind ein erfahrener Bibliothekar. Extrahieren Sie Buchinformationen.",
      },
      { role: "user", content: text },
    ],
    responseFormat: BookSchema,
    temperature: 0,
  });

  // 4. Das geparsedte Objekt abrufen
  const message = response.choices?.[0]?.message;
  if (!message?.parsed) {
    throw new Error("Antwort fehlt, wurde abgetrennt oder entspricht nicht dem Schema");
  }
  // Das rohe JSON bleibt in message.content verfügbar
  console.log(`Rohes JSON: ${message.content}`);
  // parsed ist als unknown typisiert: BookSchema.parse() gibt ein typisiertes Book zurück
  return BookSchema.parse(message.parsed);
}

// 5. Verwendung
async function main() {
  const book = await extractBook(
    "Ich habe '1984' von George Orwell geliebt, veröffentlicht 1949. Ein dystopischer Klassiker."
  );

  console.log(`Titel: ${book.title}`);
  console.log(`Autoren: ${book.authors.join(", ")}`);
  console.log(`Jahr: ${book.year}`);
  console.log(`Gattung: ${book.genre}`);
}

main();

Felder mit .describe() beschreiben

In Zod erfüllt .describe() dieselbe Funktion wie Field(description=...) in Pydantic: Es sagt dem Modell, was das Feld enthalten sollte, insbesondere wenn der Name allein mehrdeutig ist. Ein score ohne Beschreibung könnte auf eine Skala von 0–100 landen; mit einer expliziten Erwähnung des Bereichs folgt es Ihrer Konvention.

const SentimentSchema = z.object({
  sentiment: z.enum(["positive", "negative", "neutral"]).describe(
    "Gesamte Stimmung des Textes"
  ),
  score: z.number().describe(
    "Vertrauensscore zwischen 0,0 und 1,0"
  ),
  keywords: z.array(z.string()).describe(
    "Die 3–5 wichtigsten Schlüsselwörter"
  ),
  summary: z.string().describe(
    "Zusammenfassung in maximal einem Satz"
  ),
});

Fehlerbehandlung

Das Retry-Muster folgt derselben Logik wie in Python, mit einer generischen Signatur, die es für jedes Schema wiederverwendbar macht. Es besteht kein Bedarf, eine ZodError abzufangen: chat.parse() wirft keine; es lässt parsed leer. Dieser Fall, zusammen mit Netzwerk- oder HTTP-Fehlern, löst ein Retry aus. Die Einschränkung z.ZodType<T> verknüpft das als Argument übergebene Schema mit dem Rückgabetyp: schema.parse() gibt ein T zurück, sodass der Aufrufer ein korrekt typisiertes Objekt erhält, ohne zusätzliche Annotationen.

// Fortsetzung derselben Datei: client, z und BookSchema sind bereits definiert

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: "Extrahieren Sie die angeforderten Informationen." },
          { role: "user", content: text },
        ],
        responseFormat: schema,
        temperature: 0,
      });

      const parsed = response.choices?.[0]?.message?.parsed;
      if (!parsed) {
        throw new Error("Antwort fehlt oder entspricht nicht dem Schema");
      }
      return schema.parse(parsed); // stellt Typ T wieder her
    } catch (error) {
      console.error(`Versuch ${attempt + 1} fehlgeschlagen:`, error);
      if (attempt === maxRetries) throw error;
    }
  }
  throw new Error("Unerwarteter Fehler");
}

// Verwendung
try {
  const book = await safeExtract("Mein Text hier", BookSchema);
  console.log(book.title);
} catch (error) {
  console.error("Extraktion fehlgeschlagen:", error);
}

Kernaussagen

  • Zod ist das TypeScript-Äquivalent von Pydantic für Custom Structured Outputs
  • z.infer<typeof Schema> generiert den TypeScript-Typ automatisch
  • .describe() fügt Feldanweisungen hinzu, ähnlich wie Field(description=...) in Python
  • Das Mistral TypeScript SDK verwendet responseFormat (camelCase) statt response_format
  • Eine nicht konformen Antwort lässt parsed als undefined zurück (keine Ausnahme): Testen Sie dies und wiederholen Sie den Vorgang bei Bedarf