Ein Schema für Validierungsregeln und TypeScript-Typen
Ein Zod-Schema beschreibt gleichzeitig, welche Werte ein Formularfeld annehmen darf und welchen TypeScript-Typ das Ergebnis nach erfolgreicher Validierung hat. Kombiniert mit VeeValidate über einen Zod-Resolver entsteht eine einzige Quelle der Wahrheit für Formulare, die sonst leicht zwischen Validierungslogik und Typdefinitionen auseinanderdriftet.
Inhaltsverzeichnis
- 1. Das Problem doppelt gepflegter Validierungsregeln
- 2. Ein Zod-Schema als einzige Quelle der Wahrheit definieren
- 3. VeeValidate-Feldkomponenten mit dem Zod-Resolver verbinden
- 4. Fehleranzeige pro Feld
- 5. Vergleich zu reinem VeeValidate ohne Schema-Bibliothek
- 6. Verschachtelte Objekte und Arrays validieren
- 7. Asynchrone Validierung mit Zod und VeeValidate
- 8. Typsicheres Submit-Handling
- 9. Partielle Schemas für Bearbeitungsformulare
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem doppelt gepflegter Validierungsregeln
Reines VeeValidate ohne Schema-Bibliothek definiert Validierungsregeln meist direkt an jedem Feld, entweder über eingebaute Regeln wie required und email oder über individuelle Validator-Funktionen. Der TypeScript-Typ des Formularwerts wird davon unabhängig separat definiert, meist als eigenes Interface oder eigener Typ, der von Hand mit den Validierungsregeln synchron gehalten werden muss. Ändert sich eine Regel, etwa eine neue Mindestlänge für ein Passwortfeld, muss diese Änderung an zwei Stellen nachgezogen werden, der Validierungslogik und der Typdefinition, was leicht auseinanderläuft.
Zod löst dieses Problem, indem ein einziges Schema sowohl die Validierungsregeln als auch die TypeScript-Typinferenz übernimmt. Aus einem Zod-Schema lässt sich über z.infer<typeof schema> automatisch der passende TypeScript-Typ ableiten, sodass Validierungsregeln und Typdefinition zwangsläufig immer synchron bleiben, weil es nur noch eine einzige Quelle gibt, aus der beides erzeugt wird.
2. Ein Zod-Schema als einzige Quelle der Wahrheit definieren
Ein typisches Zod-Schema für ein Registrierungsformular kombiniert primitive Validierungen wie z.string().min(2) für einen Namen mit spezialisierteren Validierungen wie z.string().email() für eine E-Mail-Adresse. Zod bietet außerdem Mechanismen für feldübergreifende Validierung über .refine(), etwa um zu prüfen, ob ein Passwort-Bestätigungsfeld mit dem eigentlichen Passwortfeld übereinstimmt, was in reinem VeeValidate meist eine eigene, manuell geschriebene Cross-Field-Validierungsfunktion erfordert hätte.
Sobald das Schema einmal definiert ist, lässt es sich sowohl für die Formularvalidierung selbst als auch für jede andere Stelle im Code verwenden, die denselben Datentyp erwartet, etwa eine API-Funktion, die die validierten Formulardaten an ein Backend sendet. Diese Wiederverwendbarkeit über die reine Formularvalidierung hinaus ist einer der praktischen Hauptvorteile gegenüber isolierten VeeValidate-Regeln, die nur im Kontext des jeweiligen Formulars Sinn ergeben.
// src/schemas/registerSchema.ts
import { z } from 'zod'
export const registerSchema = z
.object({
name: z.string().min(2, 'Name muss mindestens 2 Zeichen haben'),
email: z.string().email('Bitte eine gueltige E-Mail-Adresse angeben'),
password: z.string().min(8, 'Passwort muss mindestens 8 Zeichen haben'),
passwordConfirm: z.string(),
})
.refine((data) => data.password === data.passwordConfirm, {
message: 'Passwoerter stimmen nicht ueberein',
path: ['passwordConfirm'],
})
// Automatisch abgeleiteter TypeScript-Typ, immer synchron zum Schema
export type RegisterFormData = z.infer<typeof registerSchema>
3. VeeValidate-Feldkomponenten mit dem Zod-Resolver verbinden
Die Brücke zwischen Zod und VeeValidate stellt das Paket @vee-validate/zod mit seiner Funktion toTypedSchema() her. Diese Funktion wandelt ein Zod-Schema in ein Format um, das VeeValidates useForm()-Composable als validationSchema akzeptiert. Ab diesem Punkt übernimmt VeeValidate die eigentliche Formular-Orchestrierung, also das Tracking von Feldwerten, Touched- und Dirty-Zuständen sowie das Auslösen der Validierung bei Blur- oder Submit-Events, während Zod ausschließlich für die inhaltliche Validierungslogik zuständig bleibt.
Innerhalb der Formularkomponente werden einzelne Felder über VeeValidates useField() oder die deklarative <Field>-Komponente eingebunden, wobei der Feldname exakt dem Schlüssel im Zod-Schema entsprechen muss, damit VeeValidate die passende Teilvalidierung dem richtigen Feld zuordnen kann. Diese Kopplung über den Feldnamen ist der einzige Punkt, an dem Schema und Template synchron gehalten werden müssen, alles andere übernimmt der Resolver automatisch.
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { registerSchema, type RegisterFormData } from '@/schemas/registerSchema'
const { handleSubmit, errors, defineField } = useForm<RegisterFormData>({
validationSchema: toTypedSchema(registerSchema),
})
const [email, emailAttrs] = defineField('email')
const [password, passwordAttrs] = defineField('password')
const onSubmit = handleSubmit((values) => {
// values ist bereits vollständig typisiert als RegisterFormData
console.log(values.email)
})
</script>
<template>
<form @submit="onSubmit">
<input v-model="email" v-bind="emailAttrs" type="email" />
<span>{{ errors.email }}</span>
<input v-model="password" v-bind="passwordAttrs" type="password" />
<span>{{ errors.password }}</span>
</form>
</template>
4. Fehleranzeige pro Feld
VeeValidate stellt Fehlermeldungen über das errors-Objekt aus useForm() zur Verfügung, wobei jeder Schlüssel dem jeweiligen Feldnamen entspricht und der Wert die von Zod erzeugte, lesbare Fehlermeldung enthält, sofern das Schema eine eigene Nachricht wie im Beispiel oben definiert. Diese Fehlermeldungen lassen sich direkt im Template unter dem jeweiligen Feld anzeigen, ohne dass eine zusätzliche Übersetzungsebene zwischen Zod und der Anzeige nötig ist.
Für feldübergreifende Validierungen wie den Passwort-Abgleich aus dem Beispiel oben sorgt der path-Parameter in .refine() dafür, dass der Fehler dem richtigen Feld zugeordnet wird, in diesem Fall passwordConfirm, statt als allgemeiner, nicht feldspezifischer Formularfehler zu erscheinen. Ohne diesen Parameter würde Zod den Fehler auf Objektebene melden, was für die feldweise Anzeige in der Praxis meist unpraktisch ist, weil dann nicht klar wäre, unter welchem Eingabefeld die Meldung erscheinen soll.
5. Vergleich zu reinem VeeValidate ohne Schema-Bibliothek
Reines VeeValidate ohne Zod definiert Regeln meist über die eingebaute Regel-Syntax oder individuelle Funktionen direkt am Feld, was für einfache Formulare mit wenigen, unabhängigen Feldern durchaus ausreichend und schnell umzusetzen ist. Der Nachteil zeigt sich bei komplexeren Formularen mit feldübergreifenden Abhängigkeiten, verschachtelten Objektstrukturen oder dem Wunsch nach automatischer TypeScript-Typinferenz, wo reines VeeValidate zusätzliche, manuell gepflegte Typdefinitionen erfordert.
Die Kombination mit Zod lohnt sich besonders, sobald ein Projekt ohnehin bereits TypeScript konsequent nutzt und Formularvalidierung nicht isoliert betrachtet werden soll, sondern im Zusammenhang mit den gleichen Datenstrukturen steht, die auch für API-Aufrufe oder Zustandsverwaltung verwendet werden. Für sehr kleine, einmalige Formulare mit einem einzigen Textfeld kann der zusätzliche Schema-Aufwand dagegen unnötig wirken, dort reicht oft eine einfache eingebaute VeeValidate-Regel völlig aus.
6. Verschachtelte Objekte und Arrays validieren
Ein weiterer Vorteil von Zod zeigt sich bei komplexeren Formularstrukturen mit verschachtelten Objekten oder wiederholbaren Feldgruppen, etwa einer Adressliste mit mehreren Einträgen. Zod bildet solche Strukturen nativ über z.object() und z.array() ab, inklusive Validierung jedes einzelnen Array-Elements gegen ein eigenes Sub-Schema. VeeValidate unterstützt über useFieldArray() dynamische Feldlisten, deren Werte sich direkt gegen ein solches Array-Schema validieren lassen, ohne dass für jede mögliche Array-Länge eigene Validierungsregeln geschrieben werden müssten.
In reinem VeeValidate ohne Schema-Bibliothek müsste eine vergleichbare verschachtelte Validierung meist über verschachtelte Objektpfade in der Regel-Definition nachgebildet werden, was schnell unübersichtlich wird, sobald mehr als eine Verschachtelungsebene involviert ist. Zod hält diese Struktur dagegen deklarativ und lesbar, weil sich das Schema selbst rekursiv aus kleineren Sub-Schemas zusammensetzen lässt.
7. Asynchrone Validierung mit Zod und VeeValidate
Zod unterstützt über .refine() auch asynchrone Validierungsfunktionen, etwa um zu prüfen, ob ein gewählter Benutzername bereits vergeben ist, indem eine API-Anfrage innerhalb der Refine-Funktion ausgeführt und deren Ergebnis abgewartet wird. VeeValidate erkennt über toTypedSchema() automatisch, dass es sich um eine asynchrone Validierung handelt, und wartet entsprechend auf das Promise, bevor es den Validierungsstatus des Feldes aktualisiert.
Wichtig ist bei asynchronen Refine-Funktionen, die Validierung nicht bei jedem einzelnen Tastendruck auszulösen, weil sonst bei jedem Zeichen eine neue API-Anfrage gesendet würde. VeeValidates Konfigurationsoptionen für den Validierungszeitpunkt, etwa nur bei Blur statt bei jeder Eingabe, lassen sich unabhängig vom Zod-Schema einstellen und sollten bei asynchronen Validierungen bewusst gewählt werden, um unnötige Serverlast zu vermeiden.
8. Typsicheres Submit-Handling
Der größte praktische Gewinn zeigt sich beim Absenden des Formulars: Da handleSubmit() aus VeeValidate den Rückgabetyp des Zod-Schemas kennt, ist das values-Objekt innerhalb der Submit-Callback-Funktion bereits vollständig typisiert, ohne manuelle Typumwandlung oder Casts. Jeder Zugriff auf ein Feld wird vom TypeScript-Compiler geprüft, und ein Tippfehler im Feldnamen fällt bereits beim Kompilieren auf, statt erst zur Laufzeit als undefined-Wert sichtbar zu werden.
Dieser Effekt verstärkt sich, wenn die gleiche RegisterFormData-Typdefinition auch für die Funktion verwendet wird, die die Daten an eine API sendet. Ändert sich das Zod-Schema, etwa weil ein neues Pflichtfeld hinzukommt, meldet der TypeScript-Compiler sofort alle Stellen im Code, die diesen neuen Typ noch nicht berücksichtigen, was Inkonsistenzen zwischen Formular und API-Schicht schon vor dem ersten Testlauf sichtbar macht.
9. Partielle Schemas für Bearbeitungsformulare
Ein häufiger Praxisfall ist ein Bearbeitungsformular, bei dem im Unterschied zum Erstellungsformular nicht jedes Feld verpflichtend ausgefüllt werden muss, weil bereits vorhandene Werte unverändert bleiben dürfen. Statt ein komplett neues Schema von Hand zu pflegen, bietet Zod die Methode .partial(), die aus einem bestehenden Schema automatisch eine Variante ableitet, in der alle Felder optional sind, ohne die ursprünglichen Validierungsregeln für tatsächlich ausgefüllte Felder zu verlieren.
Dieses abgeleitete Teilschema lässt sich genauso über toTypedSchema() an VeeValidate übergeben wie das ursprüngliche vollständige Schema, wodurch ein Erstellungs- und ein Bearbeitungsformular denselben Grundregeln folgen, ohne dass Validierungslogik doppelt gepflegt werden müsste. Ändert sich später eine Regel im Basisschema, etwa eine neue Mindestlänge für den Namen, wirkt sich diese Änderung automatisch auf beide Formulare aus, weil das partielle Schema stets vom aktuellen Basisschema abgeleitet wird und nie unabhängig davon existiert.
| Aspekt | Reines VeeValidate | VeeValidate + Zod |
|---|---|---|
| Regel-Definition | pro Feld einzeln | zentral im Schema |
| TypeScript-Typen | manuell gepflegt | automatisch via z.infer |
| Feldübergreifende Regeln | eigene Validator-Funktion | .refine() im Schema |
| Wiederverwendung außerhalb des Formulars | kaum möglich | Schema überall nutzbar |
| Verschachtelte Strukturen | aufwendig nachzubilden | nativ über z.object/z.array |
Mironsoft
Vue-Architektur, Composition API und Nuxt-Performance
Vue-Anwendungen, die mit jedem Feature nicht komplizierter werden?
Wir prüfen bestehende Vue- und Nuxt-Projekte auf unstrukturierte Composables, ungenutzte Reaktivität und aufgeblähte Bundles und bauen daraus eine Architektur, die neue Features aufnimmt, ohne die Codebasis unübersichtlicher zu machen.
Architektur-Review
Composables, State-Management und Komponentenstruktur auf Wartbarkeit prüfen.
Performance-Audit
Reaktivitäts-Overhead, Bundle-Größe und Nuxt-Rendering-Strategie systematisch optimieren.
Nuxt-Integration
SSR/SSG-Setup und API-Anbindung robust und typsicher aufbauen.
10. Zusammenfassung
VeeValidate + Zod: Das Wichtigste auf einen Blick
Kernidee
ein Zod-Schema liefert Validierungsregeln und TypeScript-Typ gleichzeitig
Verbindung
toTypedSchema() aus @vee-validate/zod als Brücke zwischen beiden
Fehleranzeige
errors-Objekt pro Feldname, refine() mit path für Cross-Field-Fehler
Nutzen
lohnt sich bei TypeScript-Projekten mit komplexeren oder wiederverwendeten Datenstrukturen