Worum es geht
Im Hub hängst du Regeln an ein Feld: Pflichtfeld, nicht leer, Muster, Mindestwert und so weiter. CDMS prüft sie bei jedem Schreiben, also bei Create, PUT und PATCH. Ein Verstoß heißt Validierungsfehler, die Antwort ist 422.
Geprüft wird im System-Layer, gegen die Regeln aus den Metadaten des Modells. Die Annotationen wie @NotNull an den generierten Payload-Klassen beschreiben die Felder nur für die OpenAPI-Dokumentation.
Die Regeln und ihre Fehlerschlüssel
| Regel im Hub | Regel-ID | prüft | rule im Verstoß |
|---|---|---|---|
| Pflichtfeld | @nullable | Wert ist nicht null | cannot-be-null |
| Pflicht nur beim Anlegen | @notNullOnCreate | wie oben, nur bei Create | cannot-be-null |
| Pflicht nur beim Ändern | @notNullOnUpdate | wie oben, nur bei PUT und PATCH | cannot-be-null |
| nicht leer | @notEmpty | Text nicht leer und nicht nur Leerzeichen, Liste mit mindestens einem Eintrag | cannot-be-empty |
| Länge (Eigenschaft des Feldes) | – | Text höchstens so viele Zeichen, Standard 255 | too-long |
| Muster | @pattern | der ganze Text passt auf den regulären Ausdruck | pattern-mismatch |
| Mindestwert | @min | Zahl ≥ Grenze | min-number |
| Höchstwert | @max | Zahl ≤ Grenze | max-number |
| in der Zukunft | @future, @futureOrPresent | Datum oder Zeitpunkt nach jetzt (bzw. ab jetzt) | not-in-future |
| in der Vergangenheit | @past, @pastOrPresent | Datum oder Zeitpunkt vor jetzt (bzw. bis jetzt) | not-in-past |
| eindeutig | @unique | prüft die Datenbank beim Speichern | – (eigene Antwort, siehe unten) |
Ein paar Einzelheiten, die im Alltag zählen:
@nullableheißt Pflichtfeld, auch wenn der Name das Gegenteil vermuten lässt.- Jede Regel prüft nur ihren Werttyp: Länge und Muster nur Texte, Mindest- und Höchstwert nur Zahlen. Grenzen sind inklusive und dürfen Nachkommastellen haben (
10.5). - Bei einem reinen Datum (
LocalDate) vergleicht CDMS mit heute. Heute ist weder Zukunft noch Vergangenheit, dafür gibt es@futureOrPresentund@pastOrPresent. - Ein Wert
nullverletzt nur die Pflichtregeln. „Nicht leer“, Länge, Muster und Grenzen prüfen nur vorhandene Werte. - Die Zeitregeln setzt du in den Metadaten des Modells (YAML), die Hub-Oberfläche bietet sie nicht an.
Wann welche Regel gilt: der Scope
Viele Regeln kannst du auf einen Geltungsbereich (Scope) beschränken:
| ALWAYS | CREATE | UPDATE | |
|---|---|---|---|
| gilt bei Create | ja | ja | nein |
| gilt bei PUT | ja | nein | ja |
| gilt bei PATCH | ja, für gesendete Felder | nein | ja, für gesendete Felder |
Einen Scope haben „nicht leer“, Muster, Mindest- und Höchstwert (die beiden teilen sich einen) sowie die Zeitregeln. Beim Pflichtfeld steckt die Operation in der Regel-ID selbst: @nullable gilt immer, @notNullOnCreate und @notNullOnUpdate nur bei einer Operation. Die Länge aus der Feldeigenschaft gilt immer.
Maßgeblich ist die Operation je Objekt, nicht der Endpunkt. Schickst du in einem PATCH auf eine Firma einen neuen Mitarbeiter ohne id mit, wird dieser Mitarbeiter angelegt und mit den CREATE-Regeln geprüft.
Was geprüft wird: Create, PUT, PATCH
- jedes Feld des Modells
- ein fehlendes Feld ist
null - Defaultwerte sind schon eingesetzt, ein Feld mit Default erfüllt also die Pflicht
- jedes Feld des Modells
- ein fehlendes Feld ist
null, weil PUT es leert - eine fehlende Liste zählt als
null
- nur die gesendeten Felder
- ein fehlendes Feld bleibt, wie es ist, und wird nicht geprüft
- ein gesendetes
nullwird geprüft
Das erklärt einen häufigen Unterschied: Ein PATCH mit einem einzigen Feld geht durch, derselbe Inhalt als PUT scheitert mit cannot-be-null an allen Pflichtfeldern, die du nicht mitgeschickt hast. Siehe PUT oder PATCH?
Der Ablauf: sammeln, Hooks, prüfen
-
1Client→CDMSschickt das Objekt mit vier Einträgen in
members -
2CDMSgeht jedes Feld durch, auch in den Kindobjekten, und merkt sich jeden Verstoß, statt sofort abzubrechen
-
3HookBefore-Hooks laufen und dürfen Felder noch ändern, etwa ein Pflichtfeld füllen
-
4CDMSprüft jeden gemerkten Verstoß noch einmal gegen den Wert, der jetzt am Objekt steht
-
5CDMS→Clientbleiben Verstöße übrig → 422 mit allen auf einmal, nichts wird gespeichert
-
6CDMS→Databasekeine Verstöße → speichert, danach laufen die After-Hooks
Ein Before-Hook kann einen Verstoß also auflösen, etwa indem er ein fehlendes Pflichtfeld selbst setzt. After-Hooks laufen erst nach der Prüfung, ihre Änderungen prüft CDMS nicht mehr. Mehr zu Hooks unter Hooks: Arten und Zeitpunkte.
So sieht eine 422 aus
POST /api/rest/roster/create
{
"data": {
"name": "Frühschicht",
"members": [
{ "name": "Erste" },
{ "name": "" },
{ "name": "Dritte" },
{ }
]
},
"response": ["id"]
}{
"error": "ApiValidationException",
"messageKey": "validation-failed",
"code": "422",
"layer": "API",
"violations": [
{ "field": "members[1].name", "rule": "cannot-be-empty" },
{ "field": "members[3].name", "rule": "cannot-be-null" }
]
}So liest du den Pfad in field:
| Pfad | bedeutet |
|---|---|
name | Feld des Objekts selbst |
address.street | Feld street in der Einzelreferenz address |
members[1].name | Feld name im zweiten Eintrag der Liste members (gezählt ab 0) |
lists[2].groups[0].name | beliebig tief verschachtelt |
Der Index ist die Position in der Liste, so wie du sie geschickt hast. Hat ein Feld zwei Verstöße, stehen zwei Einträge in violations. Wie ein Client daraus markierte Formularfelder macht, steht unter Validierungsfehler ins Formular bringen.
Kindobjekte
Kindobjekte prüft CDMS mit, wenn es sie schreibt:
- Ein neues Kind ohne
idwird angelegt und mit den CREATE-Regeln seines Modells geprüft. - Ein Kind mit
id, das über die Beziehung mitgeändert wird, wird mit den UPDATE-Regeln geprüft, bei PATCH nur die gesendeten Felder. - Ein Kind, das nur per
idverknüpft wird, prüft CDMS nicht. Es wird ja nicht verändert.
Scheitert ein Kind, wird auch der gültige Elternteil nicht gespeichert. Die ganze Anfrage ist eine Transaktion. Welche Beziehung Kinder anlegen oder ändern darf, steht unter Die vier Fälle beim verschachtelten Schreiben.
Eindeutig: die Datenbank prüft
Die Regel „eindeutig“ (@unique) wird zu einem Unique-Index in der Datenbank. CDMS prüft sie nicht vorher, sondern die Datenbank beim Speichern. Deshalb sieht die Antwort anders aus:
POST /api/rest/crm/customer/create
{ "data": { "number": "K-1001", "name": "Zweite GmbH" }, "response": ["id"] }{
"error": "DataIntegrityException",
"messageKey": "already-exists",
"code": "409",
"layer": "database"
}validation-failed- CDMS prüft vor dem Speichern
- alle Verstöße auf einmal
violationsmit Feldpfad und Regel
already-exists- die Datenbank lehnt beim Speichern ab
- ein Fehler, ohne Feldbezug
- kein
violations
Entscheidungstabelle
| Werte im JSON haben den richtigen Typ | alle Regeln erfüllt (nach den Before-Hooks) | Datenbank nimmt die Zeile an | Antwort |
|---|---|---|---|
| nein | – | – | 400 invalid-value|<pfad>, bevor die Regeln geprüft werden |
| ja | nein | – | 422 validation-failed mit allen Verstößen |
| ja | ja | nein, Wert schon vorhanden | 409 already-exists |
| ja | ja | ja | 200 |
„Richtiger Typ“ heißt: Text in einem Textfeld, Zahl in einem Zahlfeld, Zeitpunkte als yyyy-MM-dd HH:mm:ss, ein Enum-Wert, den das Modell kennt. Passt ein Wert nicht, prüft CDMS die Regeln gar nicht erst und antwortet mit 400 invalid-value und dem Pfad des Feldes:
| Anfrage | Antwort |
|---|---|
POST /create mit "amount": "sieben" | 400 invalid-value|data.amount |
PATCH /update/{id} mit "dueOn": "10.02.2026" | 400 invalid-value|dueOn |
| JSON, das sich gar nicht lesen lässt | 400 malformed-json |
Körper ohne data | 400 missing-data |
Beim Einlesen von Create und PUT beginnt der Pfad bei der Wurzel des Körpers (data.amount). Bei PATCH beginnt er, wie in violations, innerhalb von data (dueOn).
Fallen
Wie es weitergeht
- Warum PUT andere Felder prüft als PATCH: PUT oder PATCH? Die null-Falle
- Felder, die beim Anlegen einen Wert bekommen: Defaultwerte
- Regeln im Hub setzen: Modellieren im Hub
- Die Fehlerform im Überblick: Antwortformat