Validierung
Kurzantwort: Validiert wird im System-Layer, feldweise, gegen die Regeln der Operation (nicht des Endpunkts) und nur für Felder, die tatsächlich geschrieben werden. Alle Verstöße eines Requests werden gesammelt und in einer einzigen 422-Antwort mit vollem Feldpfad gemeldet.
Warum im System-Layer?
Abschnitt betitelt „Warum im System-Layer?“Ein PATCH auf ein Elternobjekt kann den Create eines Kindes enthalten. Nur die Rekursion weiß, welche Operation auf welcher Ebene gerade läuft – der API-Layer sieht nur „PATCH”. Deshalb liegt die Regelprüfung dort, wo geschrieben wird.
| Operation | geprüfte Regeln |
|---|---|
CREATE | Create-Regeln |
UPDATE | Update-Regeln |
PATCH | Update-Regeln – aber nur für gesendete Felder |
Die Regeln
Abschnitt betitelt „Die Regeln“MetaFieldRules je Feld:
| Regel | Wirkung | Verstoß |
|---|---|---|
notNullOnCreate / notNullOnUpdate | Pflichtfeld je Operation | cannot-be-null |
notEmpty | Strings dürfen nicht leer/blank sein | cannot-be-empty |
fieldLength | maximale Stringlänge | too-long |
stringPattern | Regex | pattern-mismatch |
numberMin / numberMax | Zahlengrenzen | min-number / max-number |
unique | eindeutiger Wert (Datenbankebene) | Constraint-Fehler |
updatable | Feld darf geändert werden | – |
noCreate | Feld darf beim Anlegen nicht gesetzt werden | – |
defaultValue | Vorbelegung beim Anlegen | – |
Jede Regel trägt zusätzlich einen RuleScope:
| Scope | gilt bei |
|---|---|
ALWAYS | jeder Operation |
CREATE | nur CREATE |
UPDATE | nur UPDATE und PATCH |
Deklariert werden sie über die Feldannotationen @NotNull, @FieldSize,
@StringPattern, @NumberMin, @NumberMax, @Unique, @DefaultValue – siehe
Modelle und Metadaten.
flowchart TB
A["beginValidation – Sammler zurücksetzen"] --> B["Rekursion schreibt Felder"]
B --> C["je Feld: validateField(...)"]
C --> D{"Regel verletzt?"}
D -->|nein| B
D -->|ja| E["Verstoß sammeln<br/>+ Nachprüfung vormerken"]
B --> F["Before-Hooks laufen"]
F --> G["assertValid: jeden Verstoß<br/>gegen die Entity nachprüfen"]
G --> H{"noch Verstöße?"}
H -->|ja| I["422 mit allen Feldern"]
H -->|nein| J["persist / merge"]
Zwei Besonderheiten:
- Verstöße werden gesammelt, nicht geworfen. Ein Formular bekommt alle fehlerhaften Felder auf einmal statt eines pro Umlauf.
- Nachprüfung nach den Hooks. Jeder Verstoß wird erneut geprüft – diesmal gegen den Wert, der auf der Entity steht. Ein Hook, der ein fehlendes Feld nachträglich füllt, löst den Verstoß auf. Füllt er es mit etwas Unzulässigem, greift die Regel, die jetzt gebrochen wird.
Defaultwerte
Abschnitt betitelt „Defaultwerte“Fehlt beim Anlegen ein Wert, greift DefaultValueResolver mit dem
defaultValue des Feldes – vor der Validierung, damit ein vorbelegtes Feld
eine Pflichtregel erfüllt. Beim Update und Patch gilt das nicht: dort ist ein
fehlender Wert eine Aussage, keine Lücke.
Beziehungen
Abschnitt betitelt „Beziehungen“Auch Beziehungsfelder werden validiert (Vorhandensein). Beim rekursiven Schreiben gelten zusätzlich:
- referenzierte IDs müssen existieren, sonst
missing-object|<id>|<klasse>(404) - Recursive-Flags entscheiden über Anlegen/Ändern/Referenzieren, siehe Beziehungen
- der Mandanten- und Owner-Kontext muss passen – sonst ist die Zeile schlicht nicht sichtbar
Bean Validation an der API
Abschnitt betitelt „Bean Validation an der API“Zusätzlich zu den Modellregeln greift Jakarta Bean Validation an den generierten Controllern über Validierungsgruppen:
public interface OnCreate {}public interface OnUpdate {}public interface OnPatch {}public SingleResponse<CustomerDto> createJson( @Validated(OnCreate.class) @RequestBody WritePayload<CustomerCreatePayload> payload) { … }So ist zum Beispiel id in AbstractPayload mit
@NotNull(groups = OnUpdate.class) versehen: bei Create irrelevant, bei Update
Pflicht.
Fehlerantwort
Abschnitt betitelt „Fehlerantwort“{ "error": "ApiValidationException", "messageKey": "CDMS_VALIDATION_FAILED", "code": "422", "layer": "API", "violations": [ { "field": "name", "rule": "cannot-be-null" }, { "field": "orders.0.total", "rule": "min-number" } ]}Der Feldpfad enthält die Verschachtelung, damit ein Formular den richtigen Punkt markieren kann.
Datenbankseitige Prüfungen
Abschnitt betitelt „Datenbankseitige Prüfungen“Was die Datenbank ablehnt (Unique-Constraints, NOT NULL, Fremdschlüssel), kommt
als DatabaseConstraintViolationException (400) bzw. DataIntegrityException
(503) zurück – ohne Feldbezug, weil die Meldung aus der Datenbank stammt. Wo ein
Feldbezug gebraucht wird, gehört die Regel ins Modell oder in einen Hook.