Files
Portfolio/blog-posts/33-zod-schema-validation.md
T
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

481 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
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)
```typescript
// 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
```typescript
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:
1. **Runtime + Compile-Time Safety**: Validierung wo TypeScript aufhört
2. **Type Inference**: Keine doppelten Definitionen
3. **Composability**: Schemas kombinieren und erweitern
4. **Framework-Agnostisch**: React, Vue, Node.js, etc.
Für jede Anwendung mit externen Daten ist Zod unverzichtbar.
---
## Bildprompts
1. "Shield protecting data, validation concept, type-safe illustration"
2. "TypeScript and runtime validation merging, code security concept"
3. "Form with checkmarks appearing as user types, validation feedback visualization"
---
## Quellen
- [Zod Documentation](https://zod.dev/)
- [Zod GitHub](https://github.com/colinhacks/zod)
- [React Hook Form + Zod](https://react-hook-form.com/get-started#SchemaValidation)
- [tRPC + Zod Integration](https://trpc.io/docs/server/validators)