Startseite / Artikel / Schema-getriebene React-Formulare: Darstellung und Validierung aus JSON Schema

Schema-getriebene React-Formulare: Darstellung und Validierung aus JSON Schema

Wie man validierte React-Formulare direkt aus JSON Schema rendern kann, $ref-, oneOf- sowie if/then-Ausführungswege handhabt, benutzerdefinierte Widgets einbindet und gängige Validierungsfallen vermeidet.

2345 Wörter

Eine manuell erstellte React-Form dupliziert in der Regel einen bereits vorhandenen Vertrag. Das Anfrageschema der API kennt, dass email erforderlich ist und wie eine E-Mail-Adresse aussehen muss, dass age eine nicht-negative Ganzzahl ist und dass role einen von drei Werten annehmen kann. Das Erneut-Eingeben dieser Regeln in JSX, erneut in einer Validierungsbibliothek und erneut in Fehlernachrichten führt zu drei verschiedenen Quellen der Wahrheit für dieselbe Datensstruktur, und diese weichen bereits ab, sobald sich der Backend-Teil ändert.

Dieser Leitfaden behandelt die Form hingegen als abgeleitet aus JSON Schema und verwendet das Open-Source-Paket react-simple-schema-form als konkrete Implementierung. Sie werden sehen, wie $ref, allOf, oneOf sowie if/then zu dynamischen Feldern werden, wie man eigene Widgets einbindet und welche Validierungsverhalten dazu führen, dass eine generierte Form wie handgefertigt wirkt.

Warum das Schema die Form steuern sollte

Abweichungen sind vorhersehbar: Ein neues Backend-Feld erreicht niemals die Form, und eine Anfrage wie „Zeige nur die Rechnungsadresse für Zahlungen per Rechnung an“ wird zu einem useState-Flag, einer bedingten Anzeige sowie einem Validierungsabgleich, die Monate später aus dem Gleichgewicht geraten.

JSON Schema kann bereits jede dieser Regeln ausdrücken: Typen, Einschränkungen, erforderliche Felder sowie bedingte Logik. Oft handelt es sich dabei um dasselbe Dokument, mit dem das Backend Anfragen validiert und das in der OpenAPI-Spezifikation eingebettet wird. Wenn die Form daraus generiert wird, aktualisiert sich durch eine Änderung des Schemas die Benutzeroberfläche sowie ihre Validierung auf einmal.

Eine minimale generierte Form

react-simple-schema-form akzeptiert ein JSON Schema nach Draft-07 und rendernt ein validiertes Formular. Laut seiner Dokumentation hat es keine Laufzeitabhängigkeiten außer React 18, liefert eigene TypeScript-Typen und bietet eine optionale Stylesheet-Datei. Die Installation erfolgt mit einem einzigen Paket:

npm install react-simple-schema-form

Das untenstehende Beispiel beschreibt ein kleines Benutzerobjekt: einen Namen, eine E-Mail mit format: 'email', ein ganzzahliges Alter mit einem Minimum von null sowie eine durch enum begrenzte Rolle. name und email sind als erforderlich markiert. Das Komponente erhält das Schema sowie einen onSubmit-Callback und nichts weiter.

import { SchemaForm } from 'react-simple-schema-form';
import 'react-simple-schema-form/styles.css';

const schema = {
  type: 'object',
  properties: {
    name:  { type: 'string', title: 'Name' },
    email: { type: 'string', format: 'email', title: 'Email' },
    age:   { type: 'integer', minimum: 0, title: 'Age' },
    role:  { type: 'string', enum: ['Admin', 'Editor', 'Viewer'], title: 'Role' },
  },
  required: ['name', 'email'],
};

<SchemaForm schema={schema} onSubmit={(data) => save(data)} />

Aus diesem Schema generiert die Bibliothek ein Texteingabefeld, ein E-Mail-Eingabefeld, ein Zahleneingabefeld sowie ein Auswahlfeld für die Enumeration, markiert erforderliche Felder, zeigt inline-Fehler an und ruft onSubmit erst dann auf, wenn die Daten gültig sind. Das Komponente funktioniert in beiden React-Modi: Entweder werden value und onChange übergeben, um sie zu steuern, oder defaultValue, damit sie ihren eigenen Zustand verwalten kann.

Jeder Generator kann ein solches flaches Objekt verarbeiten; die eigentliche Herausforderung liegen in verschachtelten und verzweigten Schemata.

Verarbeitung von nicht-flachen Schemata

In der Produktion werden Definitionen wiederverwendet, Fragmente zusammengesetzt und je nach Datenverlauf verzweigt. Die Bibliothek klärt all das anhand der aktuellen Formulardaten vor jeder Darstellung, sodass jedes Feld nur ein geflachtes Schema erhält.

Wiederverwendung von Definitionen mit $ref und allOf

Eine gemeinsame Definition wie address kann an zwei Stellen referenziert werden und erscheint dann als zwei unabhängige Abschnitte. Schlüsselwörter, die neben einem $ref stehen, überschreiben die referenzierte Definition – daher erzeugt { "$ref": "#/definitions/address", "title": "Shipping address" } einen Adressblock mit dem Titel „Versandadresse“. Mit allOf werden die Teile tief verschmolzen: Verschachtelte Eigenschaften werden rekursiv zusammengeführt und die required-Arrays zu ihrer Vereinigung kombiniert.

oneOf als diskriminierte Union behandeln

Viele Generatoren haben Schwierigkeiten mit oneOf. Ein gut funktionierendes Muster ist eine diskriminierte Union: Jede Abzweigung weist ein gemeinsames Feld mit const auf einen festen Wert hin, und das Formular verwendet dieses Feld, um die aktive Abzweigung auszuwählen.

In dem untenstehenden Zahlungsschema ist method eine Enum mit den Werten card oder bank. Der erste Zweig setzt method auf card und erfordert einen number; der zweite setzt es auf bank und erfordert eine iban.

{
  "type": "object",
  "properties": { "method": { "type": "string", "enum": ["card", "bank"] } },
  "required": ["method"],
  "oneOf": [
    { "title": "Card", "properties": { "method": { "const": "card" }, "number": { "type": "string" } }, "required": ["number"] },
    { "title": "Bank", "properties": { "method": { "const": "bank" }, "iban":   { "type": "string" } }, "required": ["iban"] }
  ]
}

Durch den Wechsel von method von card auf bank wird das Feld für die Kartennummer durch das IBAN-Feld ersetzt. In Ihrem Code gibt es weder einen Komponentenzustand noch bedingtes JSX; allein das Schema veranlasst den Wechsel. Ein oneOf, dessen Zweige nur const-Werte enthalten, wird als beschrifteter Auswahlmenü angezeigt.

Bedingte Abschnitte mit if/then/else und dependencies

Bedingte Schlüsselwörter werden jedes Mal neu bewertet, wenn sich die Daten ändern – auch bei jeder Tastenbetätigung. Ein nützlicher Ansatz ist ein optionales Feld, das erst dann validiert wird, wenn der Benutzer es aktiviert. Der folgende Fragment definiert ein schedule-Objekt mit einem booleschen enabled-Flag (standardmäßig false) sowie zwei Feldern für den Wochentag. Die if-Klausel tritt ein, wenn enabled den Wert true hat, und die then-Klausel macht in diesem Fall monday und tuesday erforderlich.

"schedule": {
  "type": "object",
  "properties": {
    "enabled": { "type": "boolean", "title": "Enable schedule", "default": false },
    "monday":  { "type": "string", "title": "Monday" },
    "tuesday": { "type": "string", "title": "Tuesday" }
  },
  "if":   { "properties": { "enabled": { "const": true } }, "required": ["enabled"] },
  "then": { "required": ["monday", "tuesday"] }
}

Wenn der Schalter ausgeschaltet ist, sind keine Angaben innerhalb des Abschnitts erforderlich und die Abgabe wird nicht blockiert. Wenn er eingeschaltet ist, erhalten beide Datumsfelder Markierungen als erforderlich und das Formular kann nur dann abgesendet werden, wenn diese ausgefüllt sind. Da der Schalter Teil der Daten und nicht des lokalen UI-Zustands ist, kann der Server denselben Datensatz mit derselben Struktur validieren und zum selben Ergebnis kommen.

Die Zeile "required": ["enabled"] innerhalb von if lässt sich leicht weglassen, ist aber unerlässlich zum Beibehalten der Funktionalität. In JSON Schema beschränken properties nur die vorhandenen Schlüssel. Ein Objekt ohne Schlüssel enabled erfüllt daher die Bedingung { "properties": { "enabled": { "const": true } } }, wodurch der then-Teil aktiviert wird und die Felder als erforderlich gelten, obwohl der Abschnitt nie aktiviert wurde. Die Erfordernissetzung des Schlüssels innerhalb der Bedingung schließt diese Lücke.

Widgets auswählen und anpassen

Ein generiertes Formular ist nur praktikabel, wenn Sie kontrollieren können, welcher Eingabefeld für welche Angabe verwendet wird. Die Bibliothek führt ein Verzeichnis der integrierten Widgets, darunter text, email, number, select, radio, checkboxes, textarea und date, und bietet drei Möglichkeiten, sie zuzuweisen:

  1. Eine uiSchema-Eigenschaft, die nach Pfad keygt und globale Suchmöglichkeiten unterstützt. tags.* bezieht sich auf jeden Eintrag in einem Array, während **.postalCode auf jede Postleitzahl in jeder Tiefe verweist – auch innerhalb eines $ref, der an zwei Stellen verwendet wird. Wenn mehrere Schlüssel übereinstimmen, gewinnt der spezifischste.
  • In das Schema eingebettete Hinweise. Ein Knoten kann seine eigenen ui:*-Schlüsselwörter enthalten, und ein Elternknoten kann ein verschachteltes uiSchema speichern, das über Kindernamen referenziert wird, sodass jeder, der auf eine gemeinsame Definition verweist, deren Kinder neu gestalten kann.
  • Eine resolveWidget-Funktion für regelbasierte Entscheidungen, wie zum Beispiel „jeder Integer mit format: epoch verwendet das Epoch-Widget“. Sie erhält das vollständig aufgelöste Schema und kann entweder einen Widget-Name oder ein Komponentenobjekt zurückgeben.
  • Die Reihenfolge der Prioritäten ist festgelegt: das uiSchema der Anwendung hat Vorrang vor in das Schema eingebetteten Hinweisen, diese wiederum vor resolveWidget-Regeln, und diese schließlich vor den Standardwerten. Diese Vorhersehbarkeit ist wichtig, wenn ein anderes Team das Schema bereitstellt – der Client kann seine eigenen Hinweise jederzeit überschreiben.

    Einen benutzerdefinierten Widget schreiben

    Ein Widget ist eine Komponente, die den aktuellen Wert sowie einen onChange-Callback erhält, zusätzlich zu Eigenschaften wie id, required, disabled und onBlur. Das untenstehende Beispiel speichert einen Zeitstempel in Form von Unix-Sekunden, zeigt dem Benutzer jedoch ein natives datetime-local-Auswahlfenster. Es wandelt die Sekunden in einen Datumsstring um, der angezeigt wird, und konvertiert bei Änderungen den Eingabewert wieder zurück, indem es Millisekunden durch 1000 teilt und undefined überlässt, wenn der Eingabewert leer oder ungültig ist. Das Widget wird unter dem Namen epoch registriert und über uiSchema dem Feld startsAt zugeordnet.

    import type { Widget } from 'react-simple-schema-form';
    
    const EpochWidget: Widget<number | undefined> = ({ id, value, onChange, onBlur, required, disabled }) => (
      <input
        type="datetime-local"
        id={id}
        required={required}
        disabled={disabled}
        value={value === undefined ? '' : new Date(value * 1000).toISOString().slice(0, 16)}
        onBlur={onBlur}
        onChange={(e) => {
          const ms = new Date(e.target.value).getTime();
          onChange(Number.isNaN(ms) ? undefined : Math.floor(ms / 1000));
        }}
      />
    );
    
    <SchemaForm schema={schema} widgets={{ epoch: EpochWidget }} uiSchema={{ startsAt: { widget: 'epoch' } }} />
    

    Das Schema gibt integer an, der Benutzer sieht ein Auswahlfeld, und die Daten enthalten Unix-Sekunden. Eine Einschränkung: toISOString() liefert Werte im UTC-Zeitraum, während sowohl ein datetime-local-Eingabefeld als auch new Date(e.target.value) mit der lokalen Zeitzone des Benutzers arbeiten. Außerhalb von UTC wird die angezeigte Zeit durch den Zeitzonenunterschied verschoben, und jede Änderung führt zu einer Anpassung des gespeicherten Wertes. Formatieren Sie daher den Anzeigewert aus den lokalen Datumsbestandteilen, damit beide Richtungen übereinstimmen.

    Ein Widget kann außerdem ein ganzes Objekt oder Array besitzen, wobei der gesamte Wert samt allen eingebetteten Fehlern übernommen wird und die Kinderkomponenten über das exportierte <Field>-Element dargestellt werden. So erhält der Zeitplan-Bereich sein Schalten- und Verbergen-Verhalten, ohne dass die Bibliothek etwas von Zeitplänen weiß.

    Falls ein Schema auf ein Widget verweist, das nie registriert wurde, protokolliert die Bibliothek eine einzige Warnung und wechselt auf den Standardeingabefeld. Ein Tippfehler in einem von einem anderen Team bereitgestellten Schema sollte sanft abgefedert werden, anstatt die Seite zum Absturz zu bringen.

    Validierung, die den Erwartungen der Benutzer entspricht

    Der eingebaute Validator ist klein und unabhängig von Abhängigkeiten; der größte Teil seiner Konzeption bezieht sich darauf, wann Fehler gemeldet werden sollen, anstatt darauf, ob sie überhaupt vorhanden sind.

    Fehler zum richtigen Zeitpunkt anzeigen

    Fehler erscheinen, nachdem der Benutzer ein Feld verlassen hat, oder gemeinsam nach einem Versuch zur Übermittlung – niemals bei der ersten Darstellung. Wenn die Übermittlung fehlschlägt, wechselt der Fokus auf das erste ungültige Feld.

    Unberührte optionale Objekte als abwesend behandeln

    Zur Darstellung von Eingabefeldern für verschachtelte Objekte füllt das Formular diese mit {} auf. Ein naiver Validator würde dann für eine optionale Adresse, an der der Benutzer nie gearbeitet hat, street und city verlangen. Die Lösung besteht darin, ein optionales Objekt, dessen Werte alle leer sind, als nicht vorhanden zu betrachten, sodass keine Fehler entstehen. Ein erforderliches Objekt wird stets validiert und listet stattdessen genau auf, welche Unterobjekte fehlen, anstatt nur eine vage Meldung wie „Adresse erforderlich“ anzugeben.

    Fehler des Attributs oneOf für die aktive Abteilung

    Wenn keine oneOf-Abteilung gültig ist, ist eine allgemeine Meldung wie „Die Daten müssen genau einem Schema entsprechen“ für den Benutzer nutzlos. Stattdessen bestimmt der Validator, zu welcher Abteilung die Daten gehören, indem er auf Diskriminatoren und Typen achtet, aber required-Eigenschaften ignoriert, und gibt die fehlerhaften Felder dieser Abteilung an. Bei einer Zahlung mit method: card ohne Kartennummer erscheint der Fehler im Feld für die Kartennummer, wo der Benutzer nachschauen wird.

    Lassen Sie niemals versteckte Felder die Übermittlung blockieren

    Übrig gebliebene, halb eingegebene Werte in einem deaktivierten Abschnitt sollten keinen pattern-Check verfehlen, den der Benutzer nicht sehen kann. Die Regel befindet sich im Schema, doch die Lösung liegt im Widget: Es leert den Abschnitt, wenn dieser deaktiviert ist, und die errors-Eigenschaft teilt dem Widget mit, welche Fehler sich im versteckten Teil befinden.

    Wiederholte Nutzung der Regeln außerhalb von React

    Der Validator wird auch eigenständig exportiert. validate(schema, data) gibt eine Liste von { path, keyword, message }-Einträgen zurück, sodass dieselben Regeln in einem Node.js-Dienst, in einem Unit-Test oder vor der Darstellung irgendetwas verwendet werden können. Für eine auf TypeScript ausgerichtete Alternative siehe das Teilen eines Zod-Schemas zwischen React und Node.

    Dokumentation für Code-Assistenten

    Formulare werden oft mithilfe eines KI-basierten Code-Assistenten erstellt, weshalb das Paket Dokumentation bereitstellt, die sowohl für Maschinen als auch für Menschen bestimmt ist:

    • Eine Agent Skill-Datei unter skills/react-simple-schema-form/SKILL.md innerhalb des npm-Pakets, die von Tools, die das Agent Skills-Format unterstützen, aus node_modules geladen werden kann. Sie umfasst die API, die Priorisierung der Widgets, die oben genannten Beispiele sowie bekannte Probleme und hat zum Zeitpunkt der Erstellung etwa 7 kB Größe.
    • llms.txt und llms-full.txt auf der Demo-Seite, die das README, die Skills sowie jedes Beispiel-Schema in eine einzige Datei zusammenfassen, die in ein Chat-Fenster eingefügt oder von einem Dokumentations-MCP-Server indiziert werden kann.
    • JSDoc mit Beispielen zu jeder Export-Funktion, sodass der Editor bei Überfahren der Typdeklarationen die Verwendung erklärt.
    • Eine context7.json-Datei, damit das Repository sauber in Context7 indiziert werden kann.

    Dies wird nicht dazu führen, dass ein Modell eine Bibliothek auswählt, erhöht aber die Wahrscheinlichkeit, dass der erste Versuch eines Assistenten erfolgreich ist – eine Praxis, die auch für interne Bibliotheken übernommen werden sollte.

    Ausprobieren

    Die live Demo zeigt einen Schema-Editor neben dem generierten Formular, zusammen mit aktuellen Daten und Fehlern darunter. Sie enthält Beispiele für $ref, allOf, oneOf, if/then/else, dependencies sowie die Auswahl von Widgets. Das Paket ist auf npm veröffentlicht, und die Quellcode- sowie Issue-Tracker befinden sich auf GitHub. Es handelt sich um ein junges Projekt, daher sollten Sie es zunächst an Ihren eigenen Schemata testen, bevor Sie darauf vertrauen.

    Haupterkenntnisse

    • Falls eine API bereits ein JSON Schema veröffentlicht, entfernt die Erstellung des Formulars daraus doppelte Regeln und hält die Validierung in der Benutzeroberfläche sowie auf dem Server im Einklang.
    • Lösen Sie $ref, allOf, oneOf sowie bedingte Elemente anhand aktueller Daten auf, damit jedes Feld ein einfaches Schema erhält.
    • Modellieren Sie Varianteformulare als diskriminierte Unionen mit const und fügen Sie stets required zu den if-Klauseln hinzu.
    • Sichern Sie zu, dass die Auswahl der Widgets über eine klare Prioritätsreihenfolge überschrieben werden kann, insbesondere bei Schemata, die von einem anderen Team verwaltet werden.
    • Gute generierte Formulare hängen vom Zeitpunkt der Validierung ab: Berichten Sie bei Entfernen des Fokuss oder Abgabe, ignorieren Sie unveränderte optionale Objekte und weisen Sie oneOf-Fehler auf die aktive Variante hin.

    Zusätzliche Literatur

  • Eine für die Produktion bereite React-Baseline: Was jedes Paket tatsächlich tut — Einführen Sie Vite, Tailwind v4, Redux Toolkit, React Router, Jest und Prettier für eine React-Anwendung ein und verstehen Sie, warum jedes Paket sowie jede Konfigurationszeile vorhanden ist.