1 min read
Web DevelopmentZod Schema Validation: Type-Safe Validation für TypeScript
Umfassender Guide zu Zod für Schema-Validierung. TypeScript-Integration, API-Validation, Form-Handling und Best Practices für robuste Anwendungen.
ZodSchema ValidationTypeScript ValidationForm ValidationAPI ValidationRuntime Validation

Zod Schema Validation: Type-Safe Validation für TypeScript
Meta-Description: Umfassender Guide zu Zod für Schema-Validierung. TypeScript-Integration, API-Validation, Form-Handling und Best Practices für robuste Anwendungen.
Keywords: Zod, Schema Validation, TypeScript Validation, Form Validation, API Validation, Runtime Validation, Type Safety
Einführung
Zod ist die führende Schema-Validierungsbibliothek für TypeScript. Sie bietet Runtime-Validierung mit automatischer Type-Inferenz – eine perfekte Brücke zwischen TypeScript's Compile-Time-Checks und der Realität von externen Daten.
Das Problem
// TypeScript prüft nur zur Compile-Time
interface User {
name: string;
email: string;
age: number;
}
// Aber was passiert zur Runtime?
const userData = await fetch('/api/user').then(r => r.json());
// userData könnte ALLES sein - TypeScript vertraut blind
const user: User = userData; // Keine Runtime-Prüfung!Die Lösung: Zod
import { z } from 'zod';
// Schema definieren
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().positive()
});
// Type automatisch inferieren
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number }
// Runtime-Validierung
const userData = await fetch('/api/user').then(r => r.json());
const user = UserSchema.parse(userData); // Wirft bei ungültigen Daten!
// Oder mit safeParse für Error-Handling
const result = UserSchema.safeParse(userData);
if (result.success) {
console.log(result.data); // Typisiert als User
} else {
console.error(result.error.errors);
}Basis-Typen
import { z } from 'zod';
// Primitive Typen
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const bigintSchema = z.bigint();
const symbolSchema = z.symbol();
const undefinedSchema = z.undefined();
const nullSchema = z.null();
const voidSchema = z.void();
const anySchema = z.any();
const unknownSchema = z.unknown();
const neverSchema = z.never();
// Literale
const tuna = z.literal('tuna');
const twelve = z.literal(12);
const isTrue = z.literal(true);
// Enums
const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']);
type FishEnum = z.infer<typeof FishEnum>; // 'Salmon' | 'Tuna' | 'Trout'
// Native Enums
enum Fruits {
Apple,
Banana
}
const FruitEnum = z.nativeEnum(Fruits);String Validierung
const stringSchema = z.string()
// Länge
.min(5, 'Mindestens 5 Zeichen')
.max(100, 'Maximal 100 Zeichen')
.length(10, 'Exakt 10 Zeichen')
// Format
.email('Ungültige E-Mail')
.url('Ungültige URL')
.uuid('Ungültige UUID')
.cuid('Ungültige CUID')
.datetime('Ungültiges Datum')
.ip('Ungültige IP')
// Regex
.regex(/^[a-z]+$/, 'Nur Kleinbuchstaben')
// Transformationen
.trim()
.toLowerCase()
.toUpperCase()
// Custom
.refine(val => val.includes('@'), 'Muss @ enthalten');
// Praktisches Beispiel
const UsernameSchema = z.string()
.min(3, 'Username zu kurz')
.max(20, 'Username zu lang')
.regex(/^[a-zA-Z0-9_]+$/, 'Nur Buchstaben, Zahlen und _')
.toLowerCase();Number Validierung
const numberSchema = z.number()
// Constraints
.gt(0, 'Größer als 0')
.gte(0, 'Größer oder gleich 0')
.lt(100, 'Kleiner als 100')
.lte(100, 'Kleiner oder gleich 100')
.positive('Muss positiv sein')
.negative('Muss negativ sein')
.nonpositive()
.nonnegative()
.multipleOf(5, 'Muss durch 5 teilbar sein')
.int('Muss Ganzzahl sein')
.finite()
.safe(); // JavaScript safe integer
// Praktisches Beispiel: Preis
const PriceSchema = z.number()
.positive('Preis muss positiv sein')
.multipleOf(0.01, 'Maximal 2 Dezimalstellen')
.max(1000000, 'Preis zu hoch');Object Schemas
// Basis Object
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().positive()
});
// Optional & Default
const UserWithDefaultsSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().optional(), // number | undefined
role: z.string().default('user'), // Hat immer einen Wert
isActive: z.boolean().nullable() // boolean | null
});
// Extend
const AdminSchema = UserSchema.extend({
permissions: z.array(z.string())
});
// Pick & Omit
const UserNameOnly = UserSchema.pick({ name: true });
const UserWithoutAge = UserSchema.omit({ age: true });
// Partial & Required
const PartialUser = UserSchema.partial(); // Alle optional
const RequiredUser = PartialUser.required(); // Alle required
// Strict Mode
const StrictUser = UserSchema.strict(); // Wirft bei extra Keys
// Passthrough & Strip
const PassthroughUser = UserSchema.passthrough(); // Behält extra Keys
const StrippedUser = UserSchema.strip(); // Entfernt extra Keys (default)Array & Tuple
// Array
const StringArraySchema = z.array(z.string());
const NumberArraySchema = z.array(z.number())
.min(1, 'Mindestens 1 Element')
.max(10, 'Maximal 10 Elemente')
.nonempty('Darf nicht leer sein');
// Tuple
const CoordinateSchema = z.tuple([
z.number(), // x
z.number(), // y
z.number().optional() // z (optional)
]);
type Coordinate = z.infer<typeof CoordinateSchema>;
// [number, number, number?]
// Rest Elements
const StringsThenNumbers = z.tuple([z.string(), z.string()])
.rest(z.number());
// [string, string, ...number[]]Union & Discriminated Union
// Union
const StringOrNumber = z.union([z.string(), z.number()]);
// Shorthand
const StringOrNull = z.string().nullable(); // string | null
const StringOrUndefined = z.string().optional(); // string | undefined
// Discriminated Union (performanter)
const ResultSchema = z.discriminatedUnion('status', [
z.object({ status: z.literal('success'), data: z.string() }),
z.object({ status: z.literal('error'), error: z.string() })
]);
type Result = z.infer<typeof ResultSchema>;
// { status: 'success'; data: string } | { status: 'error'; error: string }Transformationen
// Transform Output
const NumberFromString = z.string().transform(val => parseInt(val, 10));
// Input: string, Output: number
// Preprocessing
const NumberSchema = z.preprocess(
(val) => {
if (typeof val === 'string') return parseInt(val, 10);
return val;
},
z.number()
);
// Praktisches Beispiel: API Response
const ApiResponseSchema = z.object({
created_at: z.string().transform(val => new Date(val)),
price_cents: z.number().transform(val => val / 100),
is_active: z.union([z.boolean(), z.literal('true'), z.literal('false')])
.transform(val => val === true || val === 'true')
});
// Input: { created_at: "2024-01-15", price_cents: 1999, is_active: "true" }
// Output: { created_at: Date, price_cents: 19.99, is_active: true }Custom Validation mit Refine
// Einfaches Refine
const PasswordSchema = z.string()
.min(8)
.refine(
(val) => /[A-Z]/.test(val),
'Muss Großbuchstaben enthalten'
)
.refine(
(val) => /[0-9]/.test(val),
'Muss Zahlen enthalten'
);
// Superrefine für komplexe Logik
const SignupSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Passwörter stimmen nicht überein',
path: ['confirmPassword']
});
}
});
// Async Validation
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
'E-Mail bereits vergeben'
);Integration mit React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const SignupSchema = z.object({
name: z.string().min(2, 'Name zu kurz'),
email: z.string().email('Ungültige E-Mail'),
password: z.string().min(8, 'Mindestens 8 Zeichen')
});
type SignupData = z.infer<typeof SignupSchema>;
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<SignupData>({
resolver: zodResolver(SignupSchema)
});
const onSubmit = (data: SignupData) => {
console.log(data); // Typsicher!
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Registrieren</button>
</form>
);
}API Route Validation (Next.js)
// app/api/users/route.ts
import { z } from 'zod';
import { NextRequest, NextResponse } from 'next/server';
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user')
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const data = CreateUserSchema.parse(body);
// data ist typsicher
const user = await createUser(data);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.errors },
{ status: 400 }
);
}
throw error;
}
}Error Handling
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email()
});
try {
UserSchema.parse({ name: 'A', email: 'invalid' });
} catch (error) {
if (error instanceof z.ZodError) {
// Formatierte Errors
console.log(error.format());
/*
{
name: { _errors: ['String must contain at least 2 character(s)'] },
email: { _errors: ['Invalid email'] }
}
*/
// Flache Error-Liste
console.log(error.flatten());
/*
{
formErrors: [],
fieldErrors: {
name: ['String must contain at least 2 character(s)'],
email: ['Invalid email']
}
}
*/
}
}Fazit
Zod bietet:
- Runtime + Compile-Time Safety: Validierung wo TypeScript aufhört
- Type Inference: Keine doppelten Definitionen
- Composability: Schemas kombinieren und erweitern
- Framework-Agnostisch: React, Vue, Node.js, etc.
Für jede Anwendung mit externen Daten ist Zod unverzichtbar.
Bildprompts
- "Shield protecting data, validation concept, type-safe illustration"
- "TypeScript and runtime validation merging, code security concept"
- "Form with checkmarks appearing as user types, validation feedback visualization"