Zum Inhalt springen

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.


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.

Operationgeprüfte Regeln
CREATECreate-Regeln
UPDATEUpdate-Regeln
PATCHUpdate-Regeln – aber nur für gesendete Felder

MetaFieldRules je Feld:

RegelWirkungVerstoß
notNullOnCreate / notNullOnUpdatePflichtfeld je Operationcannot-be-null
notEmptyStrings dürfen nicht leer/blank seincannot-be-empty
fieldLengthmaximale Stringlängetoo-long
stringPatternRegexpattern-mismatch
numberMin / numberMaxZahlengrenzenmin-number / max-number
uniqueeindeutiger Wert (Datenbankebene)Constraint-Fehler
updatableFeld darf geändert werden
noCreateFeld darf beim Anlegen nicht gesetzt werden
defaultValueVorbelegung beim Anlegen

Jede Regel trägt zusätzlich einen RuleScope:

Scopegilt bei
ALWAYSjeder Operation
CREATEnur CREATE
UPDATEnur 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:

  1. Verstöße werden gesammelt, nicht geworfen. Ein Formular bekommt alle fehlerhaften Felder auf einmal statt eines pro Umlauf.
  2. 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.

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.

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

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.

{
"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.

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.