Zod hat sich in den letzten Jahren zum Standard für TypeScript-Validierung entwickelt. Kombiniert mit Claude Code — Anthropics KI-gestützter CLI — lässt sich robuste Schema-Validierung schneller entwickeln als je zuvor. Dieser Artikel zeigt praxisnah, wie du Zod in 2026 einsetzt: von einfachen Primitives bis hin zu komplexen API-Validatoren, React Hook Form-Integrationen und strukturierter KI-Ausgabe.
🔷
TypeScript-first
Typen aus Schemas ableiten ohne Duplikate
⚡
Zero-Cost Abstraction
Kein Overhead durch Laufzeit-Validierung
🤖
Claude Code Synergien
KI generiert passende Schemas in Sekunden
🔗
Ecosystem-Integration
React Hook Form, tRPC, Hono, AI SDK
1. Zod Grundlagen & Type Inference
Zod bietet ein deklaratives API um Schemas zu definieren und daraus TypeScript-Typen abzuleiten. Claude Code versteht diese Semantik auf Anhieb und kann komplexe Schema-Strukturen aus natürlichsprachlichen Beschreibungen generieren.
Primitive Typen
Primitives
// Alle primitiven Typen in Zod
import { z } from "zod";
const StringSchema = z.string();
const NumberSchema = z.number();
const BooleanSchema = z.boolean();
const DateSchema = z.date();
const BigIntSchema = z.bigint();
const UndefinedSchema = z.undefined();
const NullSchema = z.null();
// String-Validatoren
const EmailSchema = z.string().email();
const UrlSchema = z.string().url();
const UuidSchema = z.string().uuid();
const RegexSchema = z.string().regex(/^[A-Z]{2,4}$/);
const MinMaxSchema = z.string().min(3).max(50).trim();
// Zahl-Validatoren
const PositiveSchema = z.number().positive();
const IntSchema = z.number().int().min(0).max(100);
const FloatSchema = z.number().multipleOf(0.01); // Für Währungsbeträge
Type Inference mit z.infer
Type Inference
// Schema einmal definieren — Typ automatisch ableiten
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "moderator"]),
createdAt: z.date(),
isActive: z.boolean().default(true),
});
// TypeScript-Typ aus Schema ableiten — keine Duplikate!
type User = z.infer<typeof UserSchema>;
// Result: { id: string; email: string; name: string; age: number; role: "admin" | "user" | "moderator"; createdAt: Date; isActive: boolean }
// Validierung
const result = UserSchema.safeParse({
id: "550e8400-e29b-41d4-a716-446655440000",
email: "max@example.com",
name: "Max Mustermann",
age: 30,
role: "user",
createdAt: new Date(),
});
if (result.success) {
const user: User = result.data; // Vollständig typisiert
console.log(user.email);
} else {
console.error(result.error.flatten());
}
Arrays, Unions und Enums
Collections
// Arrays
const TagsSchema = z.array(z.string()).min(1).max(10);
const NumbersSchema = z.array(z.number().positive());
// Tuple — exakte Typen an bestimmten Positionen
const CoordinateSchema = z.tuple([
z.number(), // latitude
z.number(), // longitude
]);
// Union — eines von mehreren Schemas
const StringOrNumber = z.union([z.string(), z.number()]);
// Discriminated Union — performanter als union()
const EventSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
z.object({ type: z.literal("keypress"), key: z.string() }),
z.object({ type: z.literal("scroll"), deltaY: z.number() }),
]);
// Enum — Empfohlen für feste Wertemengen
const StatusEnum = z.enum(["pending", "processing", "done", "failed"]);
type Status = z.infer<typeof StatusEnum>; // "pending" | "processing" | "done" | "failed"
// Record — Dynamische Keys
const MetadataSchema = z.record(z.string(), z.unknown());
const ScoresSchema = z.record(z.string().uuid(), z.number().min(0).max(100));
💡 Claude Code Tipp: Beschreibe deine Datenstruktur auf Deutsch — Claude Code generiert das passende Zod-Schema inklusive aller Validatoren. Prompt-Beispiel: "Erstelle ein Zod-Schema für einen Blog-Post mit Titel, Slug, optionaler Beschreibung, Veröffentlichungsdatum und Tags (maximal 5)".
2. Komplexe Schemas & Nested Objects
In realen Projekten sind einfache Primitive selten genug. Zod bietet mächtige Kompositionsmechanismen: extend, merge, pick, omit und tiefes Nesting mit voller Typsicherheit.
Schema-Komposition mit extend und merge
Komposition
// Base-Schema für wiederverwendbare Felder
const BaseEntitySchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
deletedAt: z.date().nullable().optional(),
});
// extend() — Felder zum bestehenden Schema hinzufügen
const ProductSchema = BaseEntitySchema.extend({
name: z.string().min(1).max(200),
slug: z.string().regex(/^[a-z0-9-]+$/),
price: z.number().positive().multipleOf(0.01),
stock: z.number().int().min(0),
categoryId: z.string().uuid(),
});
// Adress-Schema für Bestellungen
const AddressSchema = z.object({
street: z.string().min(1),
city: z.string().min(1),
postalCode: z.string().regex(/^\d{5}$/),
country: z.string().length(2), // ISO 3166-1 alpha-2
});
// merge() — Zwei unabhängige Schemas vereinen
const ShippingSchema = AddressSchema.merge(z.object({
carrier: z.enum(["DHL", "DPD", "UPS", "Hermes"]),
trackingNumber: z.string().optional(),
estimatedDelivery: z.date().optional(),
}));
type Product = z.infer<typeof ProductSchema>;
type Shipping = z.infer<typeof ShippingSchema>;
pick, omit und Partial
Transformation
// pick() — Nur bestimmte Felder übernehmen
const ProductPreviewSchema = ProductSchema.pick({
id: true,
name: true,
price: true,
slug: true,
});
// omit() — Bestimmte Felder ausschließen
const CreateProductSchema = ProductSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
deletedAt: true,
});
// partial() — Alle Felder optional machen (für PATCH-Requests)
const UpdateProductSchema = CreateProductSchema.partial();
// required() — Alle optionalen Felder verpflichtend machen
const FullProductSchema = UpdateProductSchema.required();
// Tief verschachteltes Beispiel — Bestellung mit Positionen
const OrderSchema = BaseEntitySchema.extend({
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
discount: z.number().min(0).max(1).default(0),
})).min(1),
shipping: ShippingSchema,
couponCode: z.string().optional(),
notes: z.string().max(500).optional(),
status: z.enum(["pending", "confirmed", "shipped", "delivered", "cancelled"]),
});
type Order = z.infer<typeof OrderSchema>;
Optional, Nullable und Default Values
Modifiers
// optional() — Feld kann fehlen (undefined)
const MaybeString = z.string().optional();
// Type: string | undefined
// nullable() — Feld kann null sein
const NullableString = z.string().nullable();
// Type: string | null
// nullish() — optional + nullable kombiniert
const NullishString = z.string().nullish();
// Type: string | null | undefined
// default() — Standardwert wenn Feld fehlt
const ConfigSchema = z.object({
port: z.number().int().min(1024).max(65535).default(3000),
debug: z.boolean().default(false),
logLevel: z.enum(["error", "warn", "info", "debug"]).default("info"),
maxConnections: z.number().positive().default(100),
allowedOrigins: z.array(z.string().url()).default([]),
timeout: z.number().positive().default(30000),
});
const config = ConfigSchema.parse({}); // Alle Defaults greifen
console.log(config.port); // 3000
console.log(config.logLevel); // "info"
⚠️ Achtung: optional() und nullable() verhalten sich unterschiedlich. optional() erlaubt fehlendes Feld (undefined), nullable() erlaubt explizit null. In einer API, die null zurückgibt, brauchst du nullable() oder nullish().
3. Custom Validators & Refinements
Wenn die eingebauten Validatoren nicht reichen, bietet Zod refine, superRefine und transform für beliebig komplexe Logik — inklusive asynchroner Validierungen und Datentransformation.
refine — Einfache Custom-Validierung
refine
// refine() für eine einzelne Bedingung
const PasswordSchema = z.string()
.min(8, "Mindestens 8 Zeichen")
.refine(
(val) => /[A-Z]/.test(val),
"Muss einen Großbuchstaben enthalten"
)
.refine(
(val) => /[0-9]/.test(val),
"Muss eine Zahl enthalten"
)
.refine(
(val) => /[^A-Za-z0-9]/.test(val),
"Muss ein Sonderzeichen enthalten"
);
// Cross-Field-Validierung mit refine auf Object-Ebene
const PasswordConfirmSchema = z.object({
password: PasswordSchema,
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: "Passwörter stimmen nicht überein",
path: ["confirmPassword"], // Fehler am richtigen Feld anzeigen
}
);
// Async refine — z.B. Datenbankprüfung
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const existing = await db.user.findUnique({ where: { email } });
return !existing;
},
"E-Mail ist bereits registriert"
);
superRefine — Komplexe Multi-Fehler-Validierung
superRefine
// superRefine() für mehrere Fehler gleichzeitig
const BookingSchema = z.object({
checkIn: z.date(),
checkOut: z.date(),
guests: z.number().int().positive(),
roomType: z.enum(["single", "double", "suite"]),
}).superRefine((data, ctx) => {
// Prüfung 1: Checkout nach Checkin
if (data.checkOut <= data.checkIn) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Checkout muss nach Checkin liegen",
path: ["checkOut"],
});
}
// Prüfung 2: Mindestaufenthalt
const nights = (data.checkOut.getTime() - data.checkIn.getTime()) / (1000 * 60 * 60 * 24);
if (nights < 2) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 2,
type: "number",
inclusive: true,
message: "Mindestaufenthalt: 2 Nächte",
path: ["checkIn"],
});
}
// Prüfung 3: Kapazitätsgrenzen
const maxGuests = { single: 1, double: 2, suite: 4 };
if (data.guests > maxGuests[data.roomType]) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `${data.roomType} erlaubt max. ${maxGuests[data.roomType]} Gäste`,
path: ["guests"],
});
}
});
transform und preprocess
transform
// transform() — Daten nach Validierung umwandeln
const TrimmedStringSchema = z.string().transform((val) => val.trim());
const SlugSchema = z.string()
.min(1)
.transform((val) => val
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/^-+|-+$/g, "")
);
// Typ-Konvertierung: string nach Date
const DateStringSchema = z.string()
.datetime()
.transform((val) => new Date(val));
// preprocess() — Rohdaten VOR der Validierung umwandeln
const CoerceNumberSchema = z.preprocess(
(val) => (typeof val === "string" ? Number(val) : val),
z.number().positive()
);
// Komplexes Transform: API-Response normalisieren
const ApiUserSchema = z.object({
user_id: z.string(),
first_name: z.string(),
last_name: z.string(),
email_address: z.string().email(),
}).transform((data) => ({
id: data.user_id,
name: `${data.first_name} ${data.last_name}`,
email: data.email_address,
}));
type NormalizedUser = z.infer<typeof ApiUserSchema>;
// { id: string; name: string; email: string }
💡 Claude Code Tipp: Claude Code erkennt, wenn Validierungslogik komplex wird, und schlägt automatisch superRefine statt mehrerer refine-Aufrufe vor. Das gibt dir bessere Fehlermeldungen und einen einzigen Pass durch die Validierung.
4. API-Validierung mit Express/Hono
Zod ist ideal für Server-seitige Validierung von HTTP-Requests. Das Middleware-Muster sorgt für saubere Separation of Concerns — Validierungslogik ist einmal definiert und überall wiederverwendbar.
Middleware-Muster für Express
Express
// validation.middleware.ts
import { Request, Response, NextFunction } from "express";
import { ZodSchema, ZodError } from "zod";
import { fromZodError } from "zod-validation-error";
export function validate(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
const error = fromZodError(result.error);
return res.status(400).json({
success: false,
error: "Validierungsfehler",
details: error.details.map((d) => ({
field: d.path.join("."),
message: d.message,
})),
});
}
req.body = result.data; // Validierte + transformierte Daten
next();
};
}
// Route mit Middleware
import { CreateProductSchema } from "./schemas/product.schema";
router.post(
"/products",
validate(CreateProductSchema),
async (req, res) => {
// req.body ist jetzt vollständig validiert und typisiert
const product = await productService.create(req.body);
res.status(201).json({ success: true, data: product });
}
);
Hono mit Zod Validator
Hono
// Hono + @hono/zod-validator
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const app = new Hono();
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
plan: z.enum(["free", "pro", "enterprise"]).default("free"),
});
const UserQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
});
// POST mit Body-Validierung
app.post(
"/users",
zValidator("json", CreateUserSchema),
async (c) => {
const body = c.req.valid("json"); // Vollständig typisiert
const user = await userService.create(body);
return c.json({ success: true, user }, 201);
}
);
// GET mit Query-Validierung
app.get(
"/users",
zValidator("query", UserQuerySchema),
async (c) => {
const query = c.req.valid("query");
const users = await userService.list(query);
return c.json({ success: true, ...users });
}
);
parse vs safeParse und Fehlerformatierung
Error Handling
// parse() — wirft ZodError bei Fehler
try {
const data = UserSchema.parse(rawInput);
// data ist validiert und typisiert
} catch (err) {
if (err instanceof ZodError) {
console.error(err.flatten());
// { formErrors: [], fieldErrors: { email: ["Invalid email"] } }
}
}
// safeParse() — kein throw, gibt Result-Objekt zurück
const result = UserSchema.safeParse(rawInput);
if (!result.success) {
// Strukturierte Fehler für API-Response
const errors = result.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
code: issue.code,
}));
// Oder mit zod-validation-error für lesbare Fehlermeldungen
const { fromZodError } = await import("zod-validation-error");
const readable = fromZodError(result.error);
console.error(readable.message);
// "Validation error: email must be a valid email address; name must be at least 2 characters"
}
// safeParseAsync() für async refine
const asyncResult = await UniqueEmailSchema.safeParseAsync(email);
💡 Best Practice: Nutze safeParse in HTTP-Handlern (kein ungewollter 500er durch unbehandelte ZodErrors) und parse in Kontexten wo du den Error-Boundary selbst kontrollierst — z.B. beim Starten der App mit ConfigSchema.parse(process.env).
Die Kombination aus React Hook Form und Zod ist der De-facto-Standard für typsichere Formulare in React. Der zodResolver verbindet beide Bibliotheken nahtlos — inklusive verschachtelter Objekte und dynamischer Arrays.
Grundlegende Integration mit zodResolver
React Hook Form
// schemas/registration.schema.ts
import { z } from "zod";
export const RegistrationSchema = z.object({
email: z.string().email("Bitte gültige E-Mail-Adresse eingeben"),
password: z.string()
.min(8, "Mindestens 8 Zeichen")
.regex(/[A-Z]/, "Mindestens ein Großbuchstabe")
.regex(/[0-9]/, "Mindestens eine Zahl"),
confirmPassword: z.string(),
acceptTerms: z.literal(true, {
errorMap: () => ({ message: "AGBs müssen akzeptiert werden" }),
}),
}).refine(
(data) => data.password === data.confirmPassword,
{ message: "Passwörter stimmen nicht überein", path: ["confirmPassword"] }
);
export type RegistrationFormData = z.infer<typeof RegistrationSchema>;
// RegistrationForm.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
export function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
watch,
reset,
} = useForm<RegistrationFormData>({
resolver: zodResolver(RegistrationSchema),
defaultValues: { email: "", password: "", confirmPassword: "" },
});
const password = watch("password"); // Live-Wert für UI-Feedback
const onSubmit = async (data: RegistrationFormData) => {
await registerUser(data);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register("email")} type="email" placeholder="E-Mail" />
{errors.email && <p className="error">{errors.email.message}</p>}
</div>
<div>
<input {...register("password")} type="password" />
{errors.password && <p className="error">{errors.password.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>Registrieren</button>
</form>
);
}
Nested Objects und Array Fields mit useFieldArray
useFieldArray
// Komplexes Formular: Bestellung mit dynamischen Positionen
const OrderFormSchema = z.object({
customer: z.object({
name: z.string().min(2),
email: z.string().email(),
phone: z.string().regex(/^\+?[\d\s\-().]{7,20}$/).optional(),
}),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive("Menge muss mindestens 1 sein"),
notes: z.string().max(200).optional(),
})).min(1, "Mindestens eine Position erforderlich"),
deliveryDate: z.date().min(new Date(), "Lieferdatum muss in der Zukunft liegen"),
});
type OrderFormData = z.infer<typeof OrderFormSchema>;
import { useForm, useFieldArray, Controller } from "react-hook-form";
function OrderForm() {
const { control, register, handleSubmit, formState: { errors } } =
useForm<OrderFormData>({ resolver: zodResolver(OrderFormSchema) });
const { fields, append, remove } = useFieldArray({
control,
name: "items",
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("customer.name")} />
{errors.customer?.name && <p>{errors.customer.name.message}</p>}
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`items.${index}.productId`)} />
<input
type="number"
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
/>
{errors.items?.[index]?.quantity &&
<p>{errors.items[index].quantity.message}</p>}
<button type="button" onClick={() => remove(index)}>Entfernen</button>
</div>
))}
<button type="button" onClick={() => append({ productId: "", quantity: 1 })}>
Position hinzufügen
</button>
<button type="submit">Bestellen</button>
</form>
);
}
watch und bedingte Validierung
Conditional Fields
// Bedingte Pflichtfelder mit discriminatedUnion
const PaymentSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal("credit_card"),
cardNumber: z.string().regex(/^\d{16}$/),
expiryMonth: z.number().int().min(1).max(12),
expiryYear: z.number().int().min(new Date().getFullYear()),
cvv: z.string().regex(/^\d{3,4}$/),
}),
z.object({
method: z.literal("paypal"),
paypalEmail: z.string().email(),
}),
z.object({
method: z.literal("bank_transfer"),
iban: z.string().regex(/^[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}$/),
bic: z.string().regex(/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/),
}),
]);
function PaymentForm() {
const { watch, register, formState: { errors } } =
useForm({ resolver: zodResolver(PaymentSchema) });
const paymentMethod = watch("method");
return (
<form>
<select {...register("method")}>
<option value="credit_card">Kreditkarte</option>
<option value="paypal">PayPal</option>
<option value="bank_transfer">Überweisung</option>
</select>
{paymentMethod === "credit_card" && (
<>
<input {...register("cardNumber")} placeholder="Kartennummer" />
{errors.cardNumber && <p>{errors.cardNumber.message}</p>}
</>
)}
{paymentMethod === "paypal" && (
<input {...register("paypalEmail")} type="email" placeholder="PayPal E-Mail" />
)}
</form>
);
}
6. Zod für OpenAI Structured Output
Mit Vercel AI SDK und generateObject kannst du LLM-Ausgaben direkt in typsichere TypeScript-Objekte umwandeln. Zod definiert das Schema — Claude oder GPT füllt es mit validierten Daten. Claude Code unterstützt diesen Workflow nativ.
generateObject mit Vercel AI SDK
AI SDK
// Strukturierte Ausgabe aus LLM extrahieren
import { generateObject } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const ProductAnalysisSchema = z.object({
summary: z.string().max(300),
category: z.enum(["electronics", "clothing", "food", "sports", "other"]),
sentiment: z.enum(["positive", "neutral", "negative"]),
keyFeatures: z.array(z.string()).max(5),
priceRange: z.object({
min: z.number().positive(),
max: z.number().positive(),
currency: z.string().length(3),
}),
targetAudience: z.array(z.string()).max(3),
confidence: z.number().min(0).max(1),
});
type ProductAnalysis = z.infer<typeof ProductAnalysisSchema>;
async function analyzeProduct(description: string): Promise<ProductAnalysis> {
const { object } = await generateObject({
model: anthropic("claude-3-5-sonnet-20241022"),
schema: ProductAnalysisSchema,
prompt: `Analysiere folgende Produktbeschreibung und gib eine strukturierte Analyse zurück: ${description}`,
});
return object; // Vollständig typisiert, garantiert valide
}
zodToJsonSchema für OpenAI Function Calling
OpenAI
// zodToJsonSchema für OpenAI Function Calling
import OpenAI from "openai";
import { zodToJsonSchema } from "zod-to-json-schema";
const WeatherSchema = z.object({
location: z.string().describe("Stadt oder Ort für die Wettervorhersage"),
days: z.number().int().min(1).max(14).describe("Anzahl der Vorhersagetage"),
unit: z.enum(["celsius", "fahrenheit"]).default("celsius"),
});
const client = new OpenAI();
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Wie wird das Wetter in Berlin?" }],
tools: [{
type: "function",
function: {
name: "get_weather",
description: "Wettervorhersage abrufen",
parameters: zodToJsonSchema(WeatherSchema),
},
}],
});
// Tool-Call-Argumente sicher validieren
if (response.choices[0].message.tool_calls) {
const args = JSON.parse(
response.choices[0].message.tool_calls[0].function.arguments
);
const validated = WeatherSchema.parse(args);
console.log(validated.location, validated.days);
}
Discriminated Union für Multi-Step Agents
Agentic AI
// Typsichere Aktionen für AI-Agents
const AgentActionSchema = z.discriminatedUnion("action", [
z.object({
action: z.literal("search"),
query: z.string().min(1),
maxResults: z.number().int().positive().default(10),
}),
z.object({
action: z.literal("create_document"),
title: z.string().min(1),
content: z.string(),
format: z.enum(["markdown", "html", "plain"]),
}),
z.object({
action: z.literal("send_email"),
to: z.array(z.string().email()).min(1),
subject: z.string().min(1).max(200),
body: z.string(),
}),
z.object({
action: z.literal("done"),
summary: z.string(),
artifacts: z.array(z.string().url()),
}),
]);
type AgentAction = z.infer<typeof AgentActionSchema>;
async function runAgentStep(context: string): Promise<AgentAction> {
const { object } = await generateObject({
model: anthropic("claude-3-5-sonnet-20241022"),
schema: AgentActionSchema,
prompt: `Bestimme die nächste Aktion: ${context}`,
});
// TypeScript kennt den exakten Typ dank discriminated union
switch (object.action) {
case "search":
return performSearch(object.query, object.maxResults);
case "create_document":
return createDocument(object.title, object.content, object.format);
case "send_email":
return sendEmail(object.to, object.subject, object.body);
case "done":
return object;
}
}
💡 Claude Code Workflow: Beschreibe Claude Code dein Datenmodell — es generiert nicht nur das Zod-Schema, sondern auch die passenden TypeScript-Typen, Validierungs-Middleware und Tests. Der gesamte Validierungsstack in einem Prompt-Durchgang.
Zusammenfassung: Zod Best Practices 2026
- Schema-first: Erst Schema definieren, dann mit
z.infer<> Typen ableiten — nie Typ und Schema duplizieren
- safeParse in HTTP-Handlern: Kein ungewollter 500er durch unbehandelte ZodErrors
- parse beim App-Start: Konfiguration mit
ConfigSchema.parse(process.env) sofort validieren
- superRefine für komplexe Logik: Mehrere Fehler gleichzeitig — bessere UX als mehrere refine-Aufrufe
- discriminatedUnion statt union: Performanter, bessere TypeScript-Inference, klarere Fehlermeldungen
- transform für Normalisierung: API-snake_case nach camelCase direkt im Schema behandeln
- zodToJsonSchema für AI-Tools: Schemas direkt als Function-Calling-Parameter nutzen
Validierungs-Modul im Kurs
Im Claude Code Mastery Kurs: vollständiges Zod-Modul mit API-Validierung, React Hook Form, Custom Validators und KI-generierter strukturierter Ausgabe.
14 Tage kostenlos testen →