Daten validieren zu müssen ist eine der wenigen Konstanten in der Webentwicklung. Ob Formulareingaben im Browser, API-Antworten vom Server oder Konfigurationsdateien – überall begegnen uns Daten, deren Struktur und Inhalt geprüft werden müssen. Zod hat sich als die führende Validierungsbibliothek im TypeScript-Ökosystem etabliert, weil es Validierung und Type-Inferenz elegant miteinander verbindet.
Was ist Zod?
Zod ist eine TypeScript-first Schema-Deklarations- und Validierungsbibliothek. Das bedeutet: Schemas werden einmal definiert, und Zod leitet daraus automatisch den passenden TypeScript-Typ ab. Dieser Ansatz eliminiert die häufige Doppelarbeit, bei der Entwickler einerseits TypeScript-Interfaces und andererseits separate Validierungslogik pflegen müssen – zwei Artefakte, die im Laufe der Zeit auseinanderdriften.
Zod wurde von Colin McDonnell entwickelt und ist auf GitHub mit über 35.000 Stars eines der meistgenutzten TypeScript-Pakete überhaupt. Die Bibliothek hat keine externen Abhängigkeiten und läuft sowohl im Browser als auch in Node.js und Edge-Runtimes wie Cloudflare Workers.
Schema-Definition
Schemas werden durch das Zod-Objekt z definiert. Die API ist bewusst einfach und selbsterklärend gehalten:
import { z } from 'zod';
// Primitive Typen
const NameSchema = z.string();
const AlterSchema = z.number().int().positive();
const AktivSchema = z.boolean();
// Objekt-Schema
const NutzerSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2).max(50),
email: z.string().email(),
alter: z.number().int().min(0).max(120).optional(),
rolle: z.enum(['admin', 'nutzer', 'gast']),
erstelltAm: z.date(),
});
// Array-Schema
const Nutzerliste = z.array(NutzerSchema);
Besonders wertvoll sind die eingebauten Validierungsregeln: .min(), .max(), .email(), .url(), .uuid(), .regex() und viele weitere decken die häufigsten Anforderungen ab, ohne dass eigene Validierungslogik geschrieben werden muss.
parse() vs. safeParse()
Zod bietet zwei Varianten zur Validierung von Daten. Die Methode .parse() gibt bei Erfolg den validierten Wert zurück und wirft bei Fehler eine ZodError-Exception:
try {
const nutzer = NutzerSchema.parse(eingabeDaten);
// nutzer ist vollständig typisiert
} catch (err) {
if (err instanceof z.ZodError) {
console.log(err.errors); // Array mit Fehlern pro Feld
}
}
Die Methode .safeParse() wirft keine Exception, sondern gibt ein Ergebnisobjekt zurück. Dieses Muster ist besonders für API-Handler und Servercode empfehlenswert:
const ergebnis = NutzerSchema.safeParse(eingabeDaten);
if (!ergebnis.success) {
// ergebnis.error enthält alle Validierungsfehler
return { fehler: ergebnis.error.flatten() };
}
// ergebnis.data ist vollständig typisiert
const nutzer = ergebnis.data;
Type-Inferenz mit z.infer
Das Herzstück von Zod ist die automatische Type-Inferenz. Mit dem Utility-Typ z.infer wird der TypeScript-Typ direkt aus dem Schema abgeleitet:
type Nutzer = z.infer<typeof NutzerSchema>;
// Entspricht exakt diesem manuell geschriebenen Interface:
// interface Nutzer {
// id: string;
// name: string;
// email: string;
// alter?: number;
// rolle: 'admin' | 'nutzer' | 'gast';
// erstelltAm: Date;
// }
Änderungen am Schema werden automatisch im abgeleiteten Typ reflektiert. Dieses Single-Source-of-Truth-Prinzip verhindert Inkonsistenzen zwischen Laufzeitvalidierung und statischer Typisierung.
Transforms und Refinements
Mit .transform() können Daten nach der Validierung umgewandelt werden:
const DatumSchema = z.string().transform((val) => new Date(val));
// Eingabe: string, Ausgabe: Date
Mit .refine() lassen sich benutzerdefinierte Validierungsregeln hinzufügen, die über die eingebauten Prüfungen hinausgehen:
const PasswortSchema = z.object({
passwort: z.string().min(8),
bestaetigung: z.string(),
}).refine((daten) => daten.passwort === daten.bestaetigung, {
message: 'Passwörter stimmen nicht überein',
path: ['bestaetigung'],
});
z.discriminatedUnion() für Union-Typen mit einem gemeinsamen Diskriminator-Feld. Diese Methode ist erheblich performanter als z.union(), weil Zod sofort den richtigen Zweig wählen kann, ohne alle Alternativen zu testen.
Integration mit React Hook Form
Zod lässt sich über den offiziellen Resolver nahtlos in React Hook Form integrieren:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const KontaktSchema = z.object({
name: z.string().min(2, 'Name muss mindestens 2 Zeichen haben'),
email: z.string().email('Ungültige E-Mail-Adresse'),
nachricht: z.string().min(10),
});
type KontaktFormular = z.infer<typeof KontaktSchema>;
function KontaktFormular() {
const { register, handleSubmit, formState: { errors } } = useForm<KontaktFormular>({
resolver: zodResolver(KontaktSchema),
});
// ...
}
React Hook Form übernimmt dabei die Formularsteuerung, während Zod die gesamte Validierungslogik kapselt. Fehler werden automatisch den richtigen Feldern zugeordnet.
Integration mit tRPC
Im tRPC-Ökosystem ist Zod der Standard für die Validierung von Procedure-Inputs. Das Schema definiert gleichzeitig die erwarteten Parameter und den TypeScript-Typ, der dem Client automatisch bekannt gemacht wird:
const nutzerRouter = router({
erstellen: publicProcedure
.input(NutzerSchema.omit({ id: true, erstelltAm: true }))
.mutation(async ({ input }) => {
// input ist vollständig typisiert
return await db.nutzer.create({ data: input });
}),
});
Vergleich: Zod vs. Yup vs. Joi vs. Valibot
| Merkmal | Zod | Yup | Joi | Valibot |
|---|---|---|---|---|
| Bundle-Größe (min+gz) | ~14 kB | ~13 kB | ~25 kB | ~7 kB (tree-shakeable) |
| TypeScript-Support | Erstklassig (TS-first) | Gut (nachträglich) | Gut (nachträglich) | Erstklassig (TS-first) |
| Type-Inferenz | Automatisch | Eingeschränkt | Eingeschränkt | Automatisch |
| Browser-Kompatibilität | Ja | Ja | Nein (Node.js-only) | Ja |
| API-Stil | Fluent / Chainable | Fluent / Chainable | Fluent / Chainable | Funktional (Pipes) |
| Ökosystem-Integration | Sehr groß (tRPC, RHF, Next.js…) | Groß (RHF, Formik) | Mittel | Wachsend |
| Async Validation | Ja | Ja | Ja | Ja |
z.string().check() die API für eigene Validierungsregeln leicht verändert. Wer von Zod v3 migriert, sollte die offizielle Migrations-Dokumentation lesen, bevor das Update eingespielt wird. Besonders .superRefine() und der Umgang mit ctx.addIssue() haben sich geändert.
Anwendungsfall: Formular- und API-Validierung
Ein typischer Anwendungsfall kombiniert clientseitige Formularvalidierung mit serverseitiger API-Validierung unter Verwendung desselben Schemas. Das Schema wird einmal in einer gemeinsamen Datei definiert und sowohl im Frontend als auch im Backend importiert:
// shared/schemas.ts
export const RegistrierungSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
passwort: z.string().min(8),
});
export type RegistrierungDaten = z.infer<typeof RegistrierungSchema>;
Dieser Ansatz garantiert, dass Frontend und Backend immer die gleichen Validierungsregeln anwenden, und verhindert inkonsistente Fehlermeldungen für den Nutzer.
Häufig gestellte Fragen
- Kann Zod auch JSON-Schema ausgeben?
- Ja, mit dem Paket
zod-to-json-schemalassen sich Zod-Schemas in JSON-Schema konvertieren. Das ist besonders nützlich für OpenAPI-Dokumentation oder die Validierung mit anderen Tools, die JSON-Schema sprechen. - Wie verhält sich Zod bei unbekannten Feldern in einem Objekt?
- Standardmäßig entfernt Zod unbekannte Felder nicht – ein Objekt mit zusätzlichen Feldern wird validiert, aber die extra Felder bleiben im geparsten Ergebnis erhalten. Mit
.strict()wird ein Fehler geworfen, wenn unbekannte Felder vorhanden sind. Mit.strip()werden unbekannte Felder entfernt. - Ist Zod für sehr große Schemas performant?
- Zod ist für typische Web-Applikationsschemas sehr performant. Bei extrem häufiger Validierung (z.B. Millionen von Datensätzen in einer Schleife) können spezialisiertere Bibliotheken wie Valibot oder Typia schneller sein. Für Standard-API-Validierung und Formulare spielt die Zod-Performance keine merkliche Rolle.
- Kann Zod mit optionalen und nullbaren Feldern umgehen?
- Ja. Mit
.optional()wird ein Feld optional (Typ:T | undefined), mit.nullable()nullbar (Typ:T | null), und mit.nullish()beides (Typ:T | null | undefined). Diese Kombinationen decken alle gängigen Szenarien in APIs und Formularen ab.