1. April 2026
Server-Driven Forms: Validierungslogik nicht duplizieren
Wenn das Frontend Backend-Geschaeftsregeln fuer die Formularvalidierung nachbaut, entsteht ein Wartungsproblem. Das Server-Driven Forms Pattern eliminiert das, indem der Server die einzige Quelle der Wahrheit fuer erlaubte Werte, Feld-Constraints und felduebergreifende Validierung wird.
Sascha Becker
Author19 Min. Lesezeit
Server-Driven Forms: Validierungslogik nicht duplizieren
Es gibt ein Problem, das vermutlich die meisten Frontend-Entwickler kennen. Du hast einen REST-Endpoint (oder eine GraphQL-Mutation), der ein komplexes Objekt entgegennimmt. Das Objekt hat konditionale Constraints: Wenn ein Attribut sich aendert, muss ein anderes anders sein. Manche Felder erlauben nur bestimmte Werte abhaengig vom aktuellen Zustand anderer Felder. Die API gibt einen fehlerhaften HTTP-Response zurueck, wenn das Objekt nicht stimmt.
Der Instinkt des Frontend-Teams ist, all diese Logik in React nachzubauen. Pflichtfelder, konditionale Anforderungen, dynamische Dropdown-Optionen, felduebergreifende Regeln. Das Ergebnis waren hunderte Zeilen Validierungscode, der das Backend spiegelte, mit der Zeit auseinanderging und auf Weisen brach, die erst auffielen, wenn der Server ablehnte, was der Client durchgewunken hatte.
Das ist ein bekannter architektonischer Code Smell. Die Industrie hat einen Namen fuer die Loesung: Server-Driven Forms.
Dieser Artikel geht das Pattern anhand eines konkreten Beispiels durch: ein Zoo-Tiertransfer-Formular. Nicht weil du Zoo-Software baust, sondern weil es die Art von Formular ist, bei der die Komplexitaet sofort offensichtlich wird und clientseitige Validierung schnell zusammenbricht.
Das Beispiel: Zoo-Tiertransfer
Stell dir vor, du baust das interne Tool fuer ein Zoo-Netzwerk, das Tiertransfers zwischen Einrichtungen verwaltet. Ein Tierpfleger muss einen Transferantrag ausfuellen. Das Formular sieht auf den ersten Blick einfach aus: Tier auswaehlen, Zielort auswaehlen, Transportmethode waehlen, Datum setzen.
Aber die Daten kommen von ueberall:
- Das Artenregister bestimmt den CITES-Schutzstatus und gesetzliche Anforderungen
- Die Einrichtungsdatenbank weiss, welche Gehege verfuegbar sind und welche Habitate sie unterstuetzen
- Das Veterinaersystem verfolgt Gesundheitszertifikate und deren Ablaufdaten
- Die Regulierungs-API liefert Import-/Exportregeln pro Land
- Das Transportsystem kennt Containeranforderungen und Carrier-Verfuegbarkeit pro Art
Und die Regeln kaskadieren. Die Art zu aendern veraendert, welche Zielorte geeignete Gehege haben. Das Zielland zu aendern veraendert, welche Gesundheitszertifikate erforderlich sind und wie aktuell sie sein muessen. Luftfracht fuer einen Primaten loest eine Container-Belueftungsanforderung aus, die fuer Reptilien nicht gilt. Eine CITES-Anhang-I-Art erfordert eine ministerielle Genehmigung, was Felder hinzufuegt, die fuer Anhang-II-Tiere nicht existieren.
Versuch mal, all das in React zu duplizieren.
Warum clientseitige Validierung scheitert
Wenn du versuchst, diese Regeln im Client abzubilden, landest du bei so etwas:
tsxfunction TransferForm({ transfer }: { transfer: Transfer }) {const [values, setValues] = useState(transfer);// dupliziert vom Backendconst isCitesI = getCitesStatus(values.speciesId) === "APPENDIX_I";const needsMinisterialApproval = isCitesI;const allowedDestinations = getDestinationsWithEnclosure(values.speciesId);const requiredCerts = getRequiredCerts(values.speciesId, values.destinationId);const allowedTransport = getTransportMethods(values.speciesId, values.weight);return (<form><Select name="species" options={allSpecies} /><Select name="destination" options={allowedDestinations} /><Select name="transport" options={allowedTransport} />{needsMinisterialApproval && <TextField name="approvalReference" />}{/* ... 15 weitere konditionale Felder */}</form>);}
Jede dieser Hilfsfunktionen ist eine Kopie von Server-Logik. getCitesStatus dupliziert das Artenregister. getDestinationsWithEnclosure dupliziert einen Join ueber die Einrichtungsdatenbank. getRequiredCerts dupliziert Regulierungsregeln, die sich aendern, wenn die Gesetzgebung aktualisiert wird.
Wenn das Regulierungsteam eine neue Quarantaeneregel fuer Amphibien hinzufuegt, wird das Backend aktualisiert. Das Frontend erfaehrt davon wochenlang nichts. In der Zwischenzeit schickt ein Tierpfleger einen Transfer ab, der die Client-Validierung besteht, aber auf dem Server mit einem rohen 400-Fehler scheitert, der nichts Nuetzliches sagt.
Das tiefere Problem ist architektonisch: Geschaeftsregeln leben an zwei Stellen.
Was zaehlt als Geschaeftsregel?
"Ein Button oeffnet einen Dialog" ist keine Geschaeftsregel. Das ist Interaktionslogik, und die gehoert ins Frontend. Geschaeftsregeln sind Domaenen-Constraints, die bestimmen, was gueltige Daten sind: welche Feldkombinationen erlaubt sind, welche Werte von anderen Werten abhaengen, welche Bedingungen erfuellt sein muessen, bevor das System eine Eingabe akzeptiert. Sie existieren, weil die Domaene es vorgibt (regulatorische Anforderungen, Datenintegritaet, organisatorische Richtlinien), nicht weil die UI sie zum Rendern braucht. Wenn diese Regeln ueber Client und Server dupliziert werden, gehen sie auseinander.
Moment. Sollte dieses Formular ueberhaupt so existieren?
Bevor du zu Server-Driven Forms greifst, stell eine haertere Frage: Warum hat dieses Formular ueberhaupt so viele felduebergreifende Abhaengigkeiten?
Ein Formular, bei dem das Aendern eines Dropdowns fuenf andere Felder umstrukturiert, ist nicht nur ein Validierungsproblem. Es ist ein UX-Problem. Der Nutzer wird gebeten, zu viele voneinander abhaengige Entscheidungen auf einem einzigen Screen zu treffen. Ein mehrstufiger Wizard, der Entscheidungen in sequenzielle Schritte aufteilt, eliminiert den Grossteil der kaskadierenden Komplexitaet:
- Schritt 1: Art auswaehlen. Festlegen. Weiter.
- Schritt 2: Zielort auswaehlen. Der Server kennt die Art bereits, also zeigt er nur gueltige Zielorte. Kein Kaskadieren. Keine deaktivierten Dropdowns. Kein "Zuerst eine Art auswaehlen"-Hilfstext.
- Schritt 3: Transport und Daten. Die Constraints sind jetzt auf das festgelegte Art-Zielort-Paar beschraenkt.
Jeder Schritt ist ein einfaches Formular ohne felduebergreifende Abhaengigkeiten. Jeder Schritt validiert isoliert. Die "wenn A sich aendert, muss B anders sein"-Regeln verschwinden, weil A bereits feststeht, bevor B erscheint.
Ehrliche Einschaetzung
Waehrend der Recherche fuer diesen Artikel habe ich den Server-Driven-Ansatz mit einem Team geteilt, das genau dieses Problem hatte. Ihre Antwort: "Das ist Overkill fuer uns. Wir duplizieren einfach die zwei Geschaeftsregeln im Frontend und ueberdenken die UX, damit das Problem verschwindet." Sie hatten recht. Fuer ihren Fall war ein saubererer Formular-Flow die bessere Loesung.
Server-Driven Forms loesen das "wir haben dieses komplexe Formular und die Validierungslogik ist dupliziert"-Problem. Gutes UX-Design verhindert, dass das Problem ueberhaupt entsteht. Wenn du dein Formular in Schritte aufteilen kannst, mach das zuerst. Wenn du es versucht hast und die Komplexitaet wirklich nicht reduzierbar ist (der Nutzer muss mehrere zusammenhaengende Felder gleichzeitig sehen und anpassen, das Formular kann nicht sequenziell sein, oder du erbst ein Formular, das bereits so gebaut ist), dann lies weiter.
Das Prinzip: Server als Single Source of Truth
Wenn du entschieden hast, dass die Formularkomplexitaet real und unvermeidbar ist, ist die Loesung nicht bessere clientseitige Validierung. Die Loesung ist, clientseitige Geschaeftslogik komplett zu entfernen. Das Frontend sollte nur zwei Dinge tun:
- UX-Validierung: "Dieses Feld ist erforderlich", "Das muss eine Zahl sein", "Mindestens 3 Zeichen". Dinge, die die Benutzererfahrung verbessern, aber keine geschaeftliche Bedeutung haben.
- Rendering: Anzeigen, was der Server sagt. Fehler, erlaubte Werte, deaktivierte Zustaende, Constraint-Meldungen.
Alles andere kommt vom Server. Welche Zielorte geeignete Gehege fuer diese Art haben. Welche Transportmethoden fuer dieses Tier legal sind. Ob das aktuelle Gesundheitszertifikat noch aktuell genug ist. Der Server beantwortet all diese Fragen, weil der Server bereits Zugriff auf alle Datenquellen hat.
Das Pattern: Server-generiertes JSON Schema
Statt ein eigenes Response-Format zu erfinden, spricht der Server eine Sprache, die ein existierender Form-Renderer bereits versteht: JSON Schema. Der Server generiert bei jedem Request dynamisch ein Schema, und der Client reicht es direkt an den Renderer weiter. Kein Custom-Mapping-Code, keine handgeschriebenen Feld-Komponenten.
Der Server gibt drei Objekte zurueck:
schema(JSON Schema): welche Felder existieren, ihre Typen, erlaubte Werte (oneOf), welche erforderlich sinduiSchema: welche Felder deaktiviert, versteckt oder mit Hilfstext versehen sindextraErrors: feldbezogene Validierungsfehler vom Server
Der Client nutzt react-jsonschema-form (RJSF) mit dem MUI-Theme, um alle drei zu rendern. Das Frontend wird zum Durchreicher.
Initialer Load (neuen Transfer erstellen)
Das Formular mounted und sendet den leeren Zustand an POST /transfer/form-state. Der Server antwortet:
json{"schema": {"type": "object","required": ["speciesId"],"properties": {"speciesId": {"type": "string","title": "Art","oneOf": [{ "const": "PAN_TROG", "title": "Schimpanse" },{ "const": "CROC_NIL", "title": "Nilkrokodil" },{ "const": "DENDRO_AZU", "title": "Blauer Pfeilgiftfrosch" }]},"destinationId": {"type": "string","title": "Zielort","oneOf": []},"transportMethod": {"type": "string","title": "Transportmethode","oneOf": []}}},"uiSchema": {"destinationId": {"ui:disabled": true,"ui:help": "Zuerst eine Art auswaehlen"},"transportMethod": {"ui:disabled": true,"ui:help": "Zuerst Art und Zielort auswaehlen"}},"extraErrors": {}}
RJSF rendert ein aktiviertes Art-Dropdown und deaktivierte Zielort-/Transport-Dropdowns mit Hilfstext. Das Frontend hat null Meinung dazu, warum der Zielort deaktiviert ist. Es rendert einfach, was der Server gesagt hat.
Nach Auswahl von "Schimpanse"
Das Formular sendet den aktualisierten Zustand. Der Server berechnet alles neu und gibt ein neues Schema zurueck:
json{"schema": {"type": "object","required": ["speciesId", "destinationId", "ministerialApprovalRef"],"properties": {"speciesId": {"type": "string","title": "Art","oneOf": [{ "const": "PAN_TROG", "title": "Schimpanse" },{ "const": "CROC_NIL", "title": "Nilkrokodil" },{ "const": "DENDRO_AZU", "title": "Blauer Pfeilgiftfrosch" }]},"destinationId": {"type": "string","title": "Zielort","oneOf": [{ "const": "ZOO_BER", "title": "Zoo Berlin" },{ "const": "ZOO_VIE", "title": "Tiergarten Schoenbrunn" }]},"transportMethod": {"type": "string","title": "Transportmethode","oneOf": []},"ministerialApprovalRef": {"type": "string","title": "Ministerielle Genehmigungsreferenz"},"healthCertificateDate": {"type": "string","format": "date","title": "Datum Gesundheitszertifikat"}}},"uiSchema": {"transportMethod": {"ui:disabled": true,"ui:help": "Zuerst einen Zielort auswaehlen"},"ministerialApprovalRef": {"ui:help": "CITES Anhang I: Ministerielle Genehmigung erforderlich"},"healthCertificateDate": {"ui:help": "Muss innerhalb von 10 Tagen vor Transport ausgestellt sein"}},"extraErrors": {}}
Die Auswahl von "Schimpanse" (eine CITES-Anhang-I-Art) hat durch das gesamte Formular kaskadiert. Zielorte wurden auf Einrichtungen mit Primatengehegen eingeschraenkt. Ein Feld fuer ministerielle Genehmigung ist im Schema mit einem required-Constraint erschienen. Ein Datumsfeld fuer das Gesundheitszertifikat ist mit Hilfstext zur Aktualitaetsregel erschienen. Das Frontend hat nichts davon berechnet. Es hat ein neues Schema empfangen und RJSF hat neu gerendert.
Einen bestehenden Transfer bearbeiten
Der gleiche Flow greift. Das Formular laedt mit gespeicherten Werten als
formData, ruft /form-state auf, und der Server gibt das Schema basierend
auf diesen Werten zurueck. Manche Felder koennen ui:disabled sein, weil der
Transfer bereits eine Teilgenehmigung erhalten hat. Das Frontend braucht keinen
separaten "Bearbeitungsmodus." Es rendert, was der Server zurueckgibt.
Validierungsfehler
Wenn der Nutzer das Formular ausfuellt, aber ein Transportdatum zu nah am Zertifikatsdatum waehlt, gibt der Server Fehler ueber extraErrors zurueck:
json{"schema": { "..." },"uiSchema": { "..." },"extraErrors": {"healthCertificateDate": {"__errors": ["Gesundheitszertifikat ist am Transportdatum aelter als 10 Tage"]},"ministerialApprovalRef": {"__errors": ["Ministerielle Genehmigungsreferenz ist fuer CITES-Anhang-I-Arten erforderlich"]}}}
RJSF rendert diese als MUI-FormHelperText-Fehlermeldungen unter jedem Feld. Das Frontend berechnet keine Zertifikats-Gueltigkeitsfenster. Es weiss nicht, was CITES Anhang I bedeutet. Der Server weiss es, und RJSF zeigt an, was er zurueckgegeben hat.
Der kaskadierende Update-Flow
Das passiert, wenn der Tierpfleger die Art mitten in der Bearbeitung von "Schimpanse" auf "Nilkrokodil" aendert:
- Nutzer waehlt "Nilkrokodil" im Art-Dropdown
- Frontend sendet die aktualisierten
formDataanPOST /transfer/form-state - Server berechnet alles neu und gibt ein neues Schema zurueck:
- Zielorte aktualisiert (nur Einrichtungen mit Reptiliengehegen ueber neue
oneOf-Werte) - Der zuvor ausgewaehlte "Tiergarten Schoenbrunn" ist moeglicherweise nicht mehr in der
oneOf-Liste - Transportmethoden aendern sich (neue
oneOf-Optionen) ministerialApprovalRefverschwindet aus dem Schema (Nilkrokodil ist CITES Anhang II, nicht I)ui:helpdes Gesundheitszertifikats aendert sich auf "Muss innerhalb von 21 Tagen vor Transport ausgestellt sein"
- Zielorte aktualisiert (nur Einrichtungen mit Reptiliengehegen ueber neue
- Frontend uebergibt das neue Schema an RJSF, das das gesamte Formular neu rendert
Wenn der zuvor ausgewaehlte Zielort nicht mehr in der oneOf-Liste ist, zeigt RJSF ihn als ungueltig an. Der Nutzer waehlt einen neuen Zielort, was einen weiteren Round-Trip ausloest, und das Formular pendelt sich in einen gueltigen Zustand ein.
Das ist die zentrale Erkenntnis: Das Formular ist ein Gespraech mit dem Server. Jede Aenderung ist eine Frage ("Was sind meine Optionen, basierend auf dem, was ich bisher ausgewaehlt habe?"), und der Server antwortet mit einem vollstaendigen JSON Schema.
Constraints dem Nutzer kommunizieren
Beachte die ui:help-Werte in den Responses oben. Das sind keine
Fehlermeldungen. Sie erklaeren, warum ein Feld existiert oder welche Regel
gilt, bevor der Nutzer etwas falsch gemacht hat. "Muss innerhalb von 10 Tagen
vor Transport ausgestellt sein" hilft dem Tierpfleger, die Anforderung zu
verstehen. "CITES Anhang I: Ministerielle Genehmigung erforderlich" erklaert,
warum gerade ein neues Feld aufgetaucht ist. Gute Server-Driven Forms
kommunizieren Regeln proaktiv, nicht nur reaktiv.
Wer macht das in Produktion?
Das ist kein theoretisches Pattern. Mehrere grosse Produkte setzen auf Server-Driven Forms:
Atlassian Jira nutzt GET /rest/api/3/issue/createmeta, um Felder, erlaubte Werte und den Required/Optional-Status fuer das Erstellen eines Issues zurueckzugeben. Wenn du den Issue-Typ aenderst, ruft das Frontend den Endpoint erneut auf, um die neue Feldkonfiguration zu holen. Jedes Dropdown, jedes Pflichtfeld, jeder Constraint kommt vom Server.
Salesforce hat ihre gesamte Plattform auf metadaten-getriebenen Formularen aufgebaut. Objekt-Schemas, Feld-Abhaengigkeiten, Validierungsregeln und Seitenlayouts sind alle server-owned. Lightning Dynamic Forms erlauben Admins, Formularlayouts deklarativ zu konfigurieren, und die Runtime holt diese Konfiguration.
Stripe nutzt teilweise server-getriebene Formulare in ihrem Payment Element. Verfuegbare Zahlungsmethoden und ihre erforderlichen Felder werden von Stripes Servern basierend auf der Haendlerkonfiguration und dem Kundenstandort abgerufen. Das Frontend rendert, was zurueckkommt.
Google Cloud APIs unterstuetzen einen validateOnly-Parameter auf Create-Methoden (dokumentiert in AIP-163), was genau dem Dry-Run-Validierungspattern entspricht.
React-Implementierung mit RJSF
Die gesamte Frontend-Implementierung besteht aus vier Paketen und einer kleinen Komponente. Keine Custom-Field-Renderer, keine Mapping-Logik.
Setup
bashpnpm add @rjsf/core @rjsf/mui @rjsf/utils @rjsf/validator-ajv8
Das sind die einzigen Abhaengigkeiten ueber das hinaus, was du bereits hast (@mui/material, @mui/icons-material, Emotion). Alle vier Pakete teilen die gleiche Versionsnummer (aktuell v6), also immer passende Versionen installieren.
Der Form-State-Hook
tsximport { useQuery } from "@tanstack/react-query";import type { RJSFSchema, UiSchema, ErrorSchema } from "@rjsf/utils";type FormStateResponse = {schema: RJSFSchema;uiSchema: UiSchema;extraErrors: ErrorSchema;};function useTransferFormState(formData: Record<string, unknown>) {return useQuery<FormStateResponse>({queryKey: ["transfer-form-state", formData],queryFn: async ({ signal }) => {const res = await fetch("/api/transfer/form-state", {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify(formData),signal,});return res.json();},staleTime: 0,});}
Die Form-Komponente
tsximport { useState } from "react";import Form from "@rjsf/mui";import validator from "@rjsf/validator-ajv8";import { useDebouncedValue } from "./useDebouncedValue";function TransferForm({ initial }: { initial?: Record<string, unknown> }) {const [formData, setFormData] = useState(initial ?? {});const debouncedData = useDebouncedValue(formData, 300);const { data: formState } = useTransferFormState(debouncedData);if (!formState) return null;return (<Formschema={formState.schema}uiSchema={formState.uiSchema}extraErrors={formState.extraErrors}formData={formData}validator={validator}noValidateonChange={({ formData }) => setFormData(formData)}onSubmit={({ formData }) => submitTransfer(formData)}/>);}
Das ist alles. Die gesamte Form-Komponente hat 20 Zeilen. Kein if (species === "PAN_TROG") irgendwo. Kein getCitesStatus. Keine Custom-<Select>-Wrapper. RJSF liest das JSON Schema und rendert MUI-Komponenten: oneOf wird zu einem Select mit MenuItems, string wird zu einem TextField, format: "date" wird zu einem Datepicker. Das uiSchema steuert deaktivierte Zustaende und Hilfstexte. Die extraErrors steuern Fehlermeldungen.
Die noValidate-Prop sagt RJSF, clientseitige JSON-Schema-Validierung komplett zu ueberspringen. Der Server ist der einzige Validierer. Du uebergibst trotzdem die validator-Prop (RJSF braucht sie fuer interne Typ-Aufloesung), aber sie blockiert nie das Absenden.
Wann den Request feuern
Nicht bei jedem Tastendruck. Sofort feuern bei Dropdown-Auswahl, Toggles und
Datepickern. Textfelder mit 300 bis 500ms debouncen. Der signal-Parameter,
der an queryFn uebergeben wird, erlaubt TanStack Query, laufende Requests
automatisch abzubrechen, wenn sich der Query Key aendert, sodass veraltete
Responses nie frische ueberschreiben.
Warum RJSF?
Wir haben mehrere schema-getriebene Form-Renderer evaluiert, bevor wir uns fuer RJSF entschieden haben:
| Library | Schema-Format | Server-Errors | MUI-Support |
|---|---|---|---|
| react-jsonschema-form | JSON Schema + uiSchema | extraErrors-Prop (beste) | Offizielles @rjsf/mui |
| JSON Forms | JSON Schema + UI Schema | additionalErrors-Prop | Offizielle Renderer |
| data-driven-forms | Eigenes flaches Schema | Final Form Internals | Offizieller Mapper |
| uniforms | JSON Schema via Bridge | Error-Object-Mapping | Offizielles Theme |
RJSF hat aus drei Gruenden gewonnen: Die extraErrors-Prop ist speziell fuer serverseitige Validierung gebaut (du uebergibst feldbezogene __errors-Arrays und sie werden als MUI-FormHelperText gerendert), die noValidate-Prop deaktiviert sauber die clientseitige Validierung, und das Schema kann bei jedem Render ausgetauscht werden ohne Probleme. Wenn der Server nach einem Artenwechsel ein komplett anderes Schema zurueckgibt, baut RJSF den Form-Tree von Grund auf neu. Keine veralteten Felder, kein verwaister State.
Was ist mit OpenAPI und GraphQL?
Wenn du Code-Generierung nutzt (wie wir in Typesafe API Code Generation for React in 2026 besprochen haben), fragst du dich vielleicht, wie dieses Pattern reinpasst.
OpenAPI
OpenAPI 3.0 kann konditionale Validierungsregeln wie "wenn die Art CITES Anhang I ist, ministerielle Genehmigung erfordern" nicht ausdruecken. Das Schema ist statisch. OpenAPI 3.1 hat JSON Schema 2020-12 uebernommen, das if/then/else, dependentRequired und dependentSchemas unterstuetzt, aber die Tool-Unterstuetzung hinkt noch hinterher. Code-Generatoren wie Hey API und Orval generieren keinen konditionalen Validierungscode aus diesen Keywords.
Der pragmatische Ansatz: Nutze deine OpenAPI-Spec fuer Types und SDK-Generierung, aber behandle den /form-state-Endpoint als separaten Belang. Definiere ihn in deiner Spec wie jeden anderen Endpoint. Der Response-Typ umschliesst schema, uiSchema und extraErrors als opake JSON-Objekte. Deine generierte SDK-Funktion gibt sie zurueck, und du reichst sie direkt an RJSF weiter.
yaml/transfer/form-state:post:summary: Form-Schema, UI-State und Validierung fuer einen Transfer abrufenrequestBody:content:application/json:schema:$ref: "#/components/schemas/TransferInput"responses:"200":content:application/json:schema:type: objectrequired: [schema, uiSchema, extraErrors]properties:schema:type: objectdescription: JSON Schema das die aktuellen Formularfelder beschreibtuiSchema:type: objectdescription: RJSF uiSchema fuer disabled/hidden/help-ZustaendeextraErrors:type: objectdescription: RJSF ErrorSchema fuer Server-Validierungsfehler
GraphQL
GraphQL hat die gleiche Einschraenkung. Das Schema-Typsystem gibt dir Non-Null-Constraints und Enum-Werte, kann aber keine felduebergreifenden konditionalen Regeln ausdruecken.
Das Pattern uebersetzt sich in eine Query, die die drei RJSF-Objekte als JSON-Scalars zurueckgibt:
graphqlscalar JSONinput TransferInput {speciesId: IDdestinationId: IDtransportMethod: StringtransportDate: DatehealthCertificateDate: DateministerialApprovalRef: String}type TransferFormState {schema: JSON!uiSchema: JSON!extraErrors: JSON!}type Query {transferFormState(input: TransferInput!): TransferFormState!}
Der Tradeoff ist, dass JSON-Scalars fuer das GraphQL-Typsystem opak sind. Du verlierst Type Safety auf der Response-Shape. In der Praxis ist das in Ordnung, weil RJSF der Consumer ist und die Schema-Struktur zur Render-Zeit validiert. Die Alternative (jeden Feldzustand als typisierten GraphQL-Typ zu modellieren) ist moeglich, schafft aber ein paralleles Typsystem, das mit den Erwartungen von RJSF synchron bleiben muss.
Validierung als Teil der Mutation
Ein alternatives GraphQL-Pattern ist, Validierungsfehler als Teil des Mutation-Payloads zurueckzugeben, nach der "User Errors as Data"-Konvention. Shopify und GitHub machen das beide so. Die Mutation gibt entweder das erstellte Objekt ODER eine Liste feldbezogener Fehler zurueck. Das funktioniert gut fuer Validierung beim Absenden, hilft aber nicht bei Echtzeit-Feedback waehrend der Nutzer das Formular ausfuellt. Fuer komplexe Formulare wie den Transferantrag brauchst du beides: die Form-State-Query fuer Echtzeit-Feedback und strukturierte Fehler auf der Mutation fuer die finale Einreichung.
Tradeoffs
Dieses Pattern ist nicht kostenlos. Hier ist, was du eintauschst:
Netzwerk-Latenz bei jeder Feldaenderung. Jede relevante Interaktion feuert einen Request. Bei langsamen Verbindungen oder VPNs mit hoher Latenz ist das spuerbar. Gegenmassnahmen: aggressives Debouncing, clientseitige Vorvalidierung fuer offensichtliche Dinge (leere Pflichtfelder), SWR-Caching, Edge-Deployment.
Kein Offline-Support. Server-Driven Forms funktionieren grundsaetzlich nicht offline. Wenn das Netzwerk nicht verfuegbar ist, kann das Formular nicht validieren oder Optionen aktualisieren. Wenn du Offline-First-Formulare brauchst, ist dieses Pattern die falsche Wahl.
Komplexitaet im Vertrag. Das Backend generiert jetzt dynamisch JSON Schema. Das ist mehr Arbeit als eine flache Fehlerliste zurueckzugeben, aber der Gewinn ist, dass das Frontend null Custom-Rendering-Logik braucht.
Testen erfordert einen Server. Du kannst Formularverhalten nicht isoliert mit Unit-Tests allein testen. Du brauchst einen laufenden Server oder einen Mock, der realistische Form-State-Responses zurueckgibt. Integrationstests werden wichtiger.
Wo JSON Schema aufhoert
JSON Schema kann nicht ausdruecken, dass "die erlaubten Zielorte davon
abhaengen, welche Einrichtungen geeignete Gehege fuer diese Art haben." Das
erfordert eine Datenbankabfrage, kein statisches Schema. Der Server handhabt
das, indem er bei jedem Request ein neues Schema mit aktualisierten
oneOf-Werten generiert. JSON Schema ist das Transportformat, nicht die
Rule-Engine. Die Regeln leben in deinem Backend-Code. JSON Schema ist nur, wie
du das Ergebnis an RJSF kommunizierst.
Wann es sich lohnt
- Formulare, die Daten aus mehreren Backend-Systemen ziehen (Artenregister, Einrichtungsdatenbanken, Regulierungs-APIs)
- Komplexe felduebergreifende Abhaengigkeiten, die kaskadieren (eine Feldaenderung invalidiert oder veraendert andere)
- Geschaeftsregeln, die sich haeufig aendern (neue Regulierungen, aktualisierte Constraints)
- Formulare mit server-only Validierung (Eindeutigkeitspruefungen, Verfuegbarkeitspruefungen, externe API-Calls)
- Multi-Tenant-Plattformen, bei denen jeder Mandant unterschiedliche Formularkonfigurationen hat
Wann nicht
- Einfache statische Formulare (Login, Kontakt, Registrierung)
- Offline-First-Anwendungen
- Formulare, bei denen alle Regeln in einem clientseitigen Schema ausdrueckbar sind (Zod, Yup)
- Formulare ohne felduebergreifende Abhaengigkeiten
Das mentale Modell
| Frage | Wer antwortet |
|---|---|
| Welche Zielorte haben Gehege fuer diese Art? | Server (generiert oneOf im Schema) |
| Ist das Gesundheitszertifikat am Transportdatum noch gueltig? | Server (gibt extraErrors zurueck) |
| Braucht diese Art ministerielle Genehmigung? | Server (fuegt Feld zum Schema + required hinzu) |
| Soll das Zielort-Dropdown deaktiviert sein? | Server (setzt ui:disabled im uiSchema) |
| Wie rendere ich all das oben? | RJSF (liest schema, uiSchema, extraErrors) |
| Wie zeige ich einen Ladezustand waehrend der Re-Validierung? | Frontend (TanStack Query isFetching) |
| Soll ich das Gewichts-Eingabefeld debouncen? | Frontend (useDebouncedValue) |
Das Frontend wird zum Durchreicher. Der Server generiert ein JSON Schema, ein uiSchema und ein Error-Objekt. RJSF rendert sie als MUI-Komponenten. Du schreibst die Validierungslogik genau einmal im Backend und bekommst konsistentes Verhalten, egal ob der Request vom React-Formular, einer mobilen App oder einem direkten API-Consumer kommt.
Die einzigen echten Kosten sind der zusaetzliche Netzwerk-Roundtrip bei Feldaenderungen. Fuer Formulare wie den Transferantrag, bei dem die Regeln fuenf verschiedene Datenquellen umspannen und sich mit jedem Regulierungs-Update aendern, ist dieser Preis nicht mal eine Frage.
Quellen und weiterfuehrende Links
- react-jsonschema-form (RJSF)
Der schema-getriebene Form-Renderer aus diesem Artikel. Das @rjsf/mui-Paket liefert das MUI-Theme.
- RJSF: Validation-Dokumentation
Wie extraErrors, noValidate und Custom-Validierung in RJSF funktionieren.
- JSON Forms
Alternativer schema-getriebener Form-Renderer von EclipseSource. Nutzt JSON Schema + UI Schema mit einer additionalErrors-Prop.
- data-driven-forms
Red Hats Form-Renderer mit einem einzelnen flachen Schema-Format. Offizieller MUI-Mapper verfuegbar.
- uniforms
Schema-agnostischer Form-Renderer von Vazco. Bridges fuer JSON Schema, GraphQL, Zod und mehr.
- TanStack Query
Data-Fetching-Library fuer den Form-State-Hook. Uebernimmt Caching, Deduplizierung und Request-Abbruch.
- Atlassian Jira: Issue Create Meta API
Jiras Endpoint, der Felder, erlaubte Werte und Required-Status pro Issue-Typ zurueckgibt. Ein Produktionsbeispiel fuer Server-Driven Forms.
- Salesforce: Lightning Dynamic Forms
Salesforces metadaten-getriebenes Formularsystem, in dem Admins Layouts deklarativ konfigurieren und die Runtime die Konfiguration holt.
- Stripe: Payment Element
Stripes server-getriebene Formularkomponente, die verfuegbare Zahlungsmethoden und erforderliche Felder basierend auf der Haendlerkonfiguration abruft.
- Google AIP-163: Validate-only Requests
Googles API-Design-Pattern fuer Dry-Run-Validierung ueber einen validateOnly-Parameter auf Create-Methoden.
- Airbnb: A Deep Dive into Server-Driven UI
Airbnbs Ghost Platform, bei der der Server Screens und Formulare als deklarative Daten sendet und der Client sie generisch rendert.
- Shopify GraphQL Admin API
Shopifys 'User Errors as Data'-Konvention, bei der Mutations strukturierte feldbezogene Fehler als Teil des Payloads zurueckgeben.
- JSON Schema 2020-12: Validation
Die JSON-Schema-Spec, die if/then/else, dependentRequired und dependentSchemas fuer konditionale Validierung eingefuehrt hat.
- OpenAPI 3.1 Specification
Die OpenAPI-Version, die JSON Schema 2020-12 uebernommen hat und konditionale Validierungs-Keywords in API-Specs ermoeglicht.
