Typinferenz, API-Design und Migrationspfade im direkten Vergleich
Wer Formulare in React 19 mit React Hook Form baut, kommt an einer Schema-Validierungsbibliothek kaum vorbei. Zod und Yup loesen dasselbe Problem mit unterschiedlichen Prioritaeten: TypeScript-first versus etablierte JavaScript-API. Dieser Artikel vergleicht beide entlang von Typinferenz, Syntax, verschachtelten Strukturen und Migrationsaufwand.
Inhaltsverzeichnis
- 1. Warum Schema-Validierung in Formularen zaehlt
- 2. Grundaufbau: zodResolver und yupResolver im Vergleich
- 3. Zods Typinferenz: z.infer als Single Source of Truth
- 4. Yups InferType: solide, aber mit Einschraenkungen
- 5. API-Design: Chaining, Fehlermeldungen und Lesbarkeit
- 6. Verschachtelte Objekte und Arrays validieren
- 7. Eigene Validierungsregeln: refine versus test
- 8. Migrationsaufwand von Yup zu Zod
- 9. Bundle-Groesse, Performance und Empfehlung
- 10. Zusammenfassung
- 11. FAQ
1. Warum Schema-Validierung in Formularen zaehlt
React Hook Form regelt Zustand und Rendering von Formularen, ueberlaesst die inhaltliche Pruefung der Eingaben aber bewusst externen Bibliotheken. Diese Trennung hat sich bewaehrt, weil Validierungsregeln oft komplexer sind als reine Pflichtfeld-Checks: Mindestlaengen, Formate, Abhaengigkeiten zwischen Feldern, asynchrone Pruefungen gegen ein Backend. Ein Schema buendelt all diese Regeln an einer Stelle, lesbar und wiederverwendbar, statt sie ueber verstreute validate-Funktionen im Formular zu verteilen.
Zod und Yup sind die beiden verbreitetsten Kandidaten fuer diese Aufgabe im React-Oekosystem. Beide bringen einen offiziellen Resolver fuer React Hook Form mit, beide beschreiben Formularstruktur deklarativ als Objektschema. Der Unterschied liegt tiefer: Zod wurde von Anfang an fuer TypeScript entworfen und leitet Typen direkt aus dem Schema ab, Yup stammt aus der reinen JavaScript-Welt und hat Typisierung nachtraeglich ergaenzt. Diese Herkunft praegt bis heute API, Fehlerverhalten und Ergonomie beider Bibliotheken.
2. Grundaufbau: zodResolver und yupResolver im Vergleich
Beide Resolver folgen demselben Muster: Sie werden ueber die resolver-Option an useForm uebergeben und uebersetzen das jeweilige Schema in das interne Fehlerformat von React Hook Form. Der Rest des Formulars, also register, handleSubmit und die Fehleranzeige ueber formState.errors, bleibt in beiden Faellen identisch. Das macht den Umstieg von einer Bibliothek zur anderen auf Formularebene erstaunlich unaufwendig, solange nur das Schema selbst ausgetauscht wird.
Der Unterschied zeigt sich erst beim Schema-Import und beim Typ, den man an useForm
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("Ungueltige E-Mail-Adresse"),
password: z.string().min(8, "Mindestens 8 Zeichen"),
});
type LoginFormValues = z.infer<typeof loginSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema),
});
const onSubmit = (values: LoginFormValues) => {
console.log(values);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<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">Anmelden</button>
</form>
);
}
3. Zods Typinferenz: z.infer als Single Source of Truth
Der groesste praktische Vorteil von Zod liegt in z.infer. Das Schema ist gleichzeitig die Laufzeitvalidierung und die Quelle fuer den TypeScript-Typ, beides bleibt automatisch synchron. Aendert sich ein Feld im Schema, etwa weil ein optionales Feld verpflichtend wird, schlaegt der TypeScript-Compiler an jeder Stelle Alarm, die noch von der alten Struktur ausgeht. Diese Kopplung verhindert eine ganze Klasse von Fehlern, bei denen Validierungsregeln und Typdefinition auseinanderlaufen.
Zod bildet dabei auch komplexere TypeScript-Konstrukte sauber ab: Unions, Literale, optionale und nullable Felder, sogar rekursive Typen ueber z.lazy. Fuer ein Formular mit bedingten Feldern, etwa einem Adressformular, das je nach Land unterschiedliche Pflichtfelder verlangt, laesst sich das ueber z.discriminatedUnion abbilden, und der abgeleitete Typ spiegelt genau diese Fallunterscheidung wider, inklusive korrekter Typeinengung im aufrufenden Code.
4. Yups InferType: solide, aber mit Einschraenkungen
Yup bietet mit InferType
Bei komplexeren Faellen zeigen sich jedoch Luecken: Bedingte Validierung ueber .when() laesst sich in Yup zwar zur Laufzeit sauber ausdruecken, der abgeleitete TypeScript-Typ bildet diese Bedingtheit aber nicht immer praezise ab, weil Yups Typsystem nachtraeglich ueber die JavaScript-API gelegt wurde statt von Grund auf typgetrieben zu sein. In solchen Faellen greifen Teams haeufig zu manuellen Typ-Overrides, was den eigentlichen Vorteil einer einzigen Quelle der Wahrheit wieder aufweicht.
5. API-Design: Chaining, Fehlermeldungen und Lesbarkeit
Beide Bibliotheken setzen auf method chaining, unterscheiden sich aber in Details, die im Alltag auffallen. Zod uebergibt Fehlermeldungen meist als letztes Argument einer Methode, etwa z.string().min(8, "Mindestens 8 Zeichen"), waehrend Yup eine eigene .required("Pflichtfeld") und .min(8, "Mindestens 8 Zeichen") Kette pflegt, bei der required explizit gesetzt werden muss, sonst gilt ein Feld standardmaessig als optional. Dieser Unterschied im Default-Verhalten ist eine haeufige Fehlerquelle bei der Migration in beide Richtungen.
Zod ist bei required strikter: Ein Feld ist standardmaessig verpflichtend, optional muss explizit ueber .optional() markiert werden. Das entspricht eher der Erwartungshaltung an ein typsicheres Schema, kann aber bei der Migration von Yup-Schemata zu unerwarteten Pflichtfeldern fuehren, wenn bestehende .required()-Aufrufe schlicht weggelassen werden. Ein sorgfaeltiger Feld-fuer-Feld-Abgleich ist bei groesseren Formularen daher unverzichtbar.
6. Verschachtelte Objekte und Arrays validieren
Formulare mit dynamischen Listen, etwa mehreren Kontaktpersonen oder Rechnungspositionen, verlangen verschachtelte Schemata mit Arrays von Objekten. Zod bildet das ueber z.array(z.object({...})) ab und kombiniert sich gut mit React Hook Forms useFieldArray, weil beide auf denselben Pfadnotationen fuer Feldnamen basieren. Fehler auf tief verschachtelten Feldern landen exakt an der erwarteten Stelle in formState.errors, ohne zusaetzliche Anpassung.
Yup verwendet dieselbe Grundidee mit yup.array().of(yup.object({...})), verhaelt sich in der Praxis aber sensibler bei fehlenden Standardwerten fuer verschachtelte Objekte; ohne explizites .default() kann InferType an dieser Stelle undefined statt eines Objekttyps liefern. Wer viel mit dynamischen, verschachtelten Formularen arbeitet, profitiert insgesamt staerker von Zods konsequenterer Typableitung, gerade wenn useFieldArray im Spiel ist.
import { z } from "zod";
import { useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const invoiceSchema = z.object({
customer: z.string().min(1, "Kunde erforderlich"),
items: z
.array(
z.object({
description: z.string().min(1, "Beschreibung erforderlich"),
amount: z.number().positive("Betrag muss positiv sein"),
})
)
.min(1, "Mindestens eine Position erforderlich"),
});
type InvoiceFormValues = z.infer<typeof invoiceSchema>;
function InvoiceForm() {
const { control, register, handleSubmit } = useForm<InvoiceFormValues>({
resolver: zodResolver(invoiceSchema),
defaultValues: { customer: "", items: [{ description: "", amount: 0 }] },
});
const { fields, append, remove } = useFieldArray({ control, name: "items" });
return (
<form onSubmit={handleSubmit((values) => console.log(values))}>
<input {...register("customer")} />
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`items.${index}.description`)} />
<input type="number" {...register(`items.${index}.amount`, { valueAsNumber: true })} />
<button type="button" onClick={() => remove(index)}>Entfernen</button>
</div>
))}
<button type="button" onClick={() => append({ description: "", amount: 0 })}>Position hinzufuegen</button>
</form>
);
}
7. Eigene Validierungsregeln: refine versus test
Sobald Standardregeln nicht ausreichen, etwa bei einem Passwort, das Sonderzeichen erfordert, oder einem Passwort-Bestaetigungsfeld, das mit dem ersten uebereinstimmen muss, brauchen beide Bibliotheken eine Erweiterung. Zod bietet dafuer .refine() auf Feld- oder Objektebene, das eine Praedikatsfunktion und eine Fehlermeldung entgegennimmt und sich beliebig mit .superRefine() fuer mehrere Fehlerpfade gleichzeitig kombinieren laesst.
Yup verwendet stattdessen .test() mit Name, Meldung und Testfunktion, konzeptionell vergleichbar, syntaktisch aber etwas umstaendlicher, weil jeder Test einen eindeutigen Namen benoetigt, um Kollisionen zwischen mehreren Tests auf demselben Feld zu vermeiden. Fuer felduebergreifende Pruefungen wie den Passwortabgleich bietet Yup ausserdem .when(), das bedingte Schemata basierend auf anderen Feldwerten erzeugt, ein Konzept, das es in Zod in dieser Form nicht gibt und dort ueber .refine() auf Objektebene nachgebaut werden muss.
8. Migrationsaufwand von Yup zu Zod
Eine Migration bestehender Yup-Formulare zu Zod laesst sich in der Praxis gut Formular fuer Formular durchfuehren, weil React Hook Form die Trennung zwischen UI und Validierung ohnehin erzwingt. Der pragmatische Weg beginnt bei den am haeufigsten geaenderten Formularen, wo der Typinferenz-Vorteil sofort spuerbar wird, und laesst stabile, selten angefasste Formulare zunaechst unangetastet in Yup, solange beide Resolver parallel im Projekt existieren duerfen.
Der eigentliche Aufwand steckt selten in den einfachen Feldern, sondern in den .when()-Konstrukten und benutzerdefinierten Tests, die felduebergreifende Logik abbilden; diese muessen sorgfaeltig auf .refine() beziehungsweise .superRefine() uebertragen und mit denselben Testfaellen erneut abgesichert werden. Ein guter Indikator fuer den Migrationsaufwand eines Formulars ist daher weniger seine Feldanzahl als die Anzahl der bedingten Validierungsregeln.
9. Bundle-Groesse, Performance und Empfehlung
Fuer die meisten Projekte ist der Laufzeitunterschied zwischen Zod und Yup bei typischen Formulargroessen kaum messbar, beide validieren synchron in Millisekundenbruchteilen. Relevanter ist die Bundle-Groesse: Zod ist in aktuellen Versionen kompakter und tree-shakebar, weil es konsequent auf funktionale Bausteine statt einer monolithischen Klassenhierarchie setzt, was sich besonders in Projekten mit vielen kleinen Formularen summiert.
Fuer neue React-19-Projekte mit TypeScript ist Zod inzwischen die naheliegendere Wahl, weil die durchgaengige Typinferenz Entwicklungsfehler frueher abfaengt und der Oekosystem-Support, etwa fuer tRPC oder OpenAPI-Generierung, breiter geworden ist. Bestehende Yup-Projekte ohne akuten Schmerzpunkt muessen deshalb aber nicht zwingend migrieren, solange Team und Codebasis mit der etablierten API produktiv arbeiten.
| Kriterium | Zod | Yup | Praxisrelevanz |
|---|---|---|---|
| TypeScript-Inferenz | Nativ ueber z.infer, sehr praezise | InferType, Luecken bei .when() | Hoch bei komplexen Formularen |
| Pflichtfeld-Default | Standardmaessig required | Standardmaessig optional | Haeufige Migrationsfalle |
| Bedingte Validierung | refine()/superRefine() auf Objektebene | .when() feldbasiert | Je nach Formularlogik unterschiedlich elegant |
| Bundle-Groesse | Kompakt, tree-shakebar | Etwas groesser, Klassen-basiert | Relevant bei vielen kleinen Formularen |
| Oekosystem | tRPC, OpenAPI-Tools wachsend | Laenger etabliert, viele Altprojekte | Team-Erfahrung zaehlt mit |
Mironsoft
React-Architektur, Performance und Magento-Frontend-Integration
React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?
Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.
Performance-Audit
Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.
State-Architektur
Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.
Magento-Integration
GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.
10. Zusammenfassung
Zod vs. Yup in RHF: Das Wichtigste auf einen Blick
Typinferenz
Zod leitet Typen direkt ueber z.infer ab, Yup benoetigt InferType mit Luecken bei bedingter Logik.
Resolver-Wechsel
zodResolver und yupResolver folgen demselben Muster, der Formularcode bleibt weitgehend unveraendert.
Pflichtfelder
Zod-Felder sind standardmaessig required, Yup-Felder standardmaessig optional, haeufigste Migrationsfalle.
Empfehlung
Neue TypeScript-Projekte profitieren von Zod, stabile Yup-Formulare muessen nicht zwingend migriert werden.