What this is about
In the hub you attach rules to a field: required, not empty, pattern, minimum value, and so on. CDMS checks them on every write, so on create, PUT and PATCH. A violation is called a validation error, and the response is 422.
The check runs in the system layer, against the rules from the model’s metadata. Annotations like @NotNull on the generated payload classes only describe the fields for the OpenAPI documentation.
The rules and their error keys
| Rule in the hub | Rule ID | checks | rule in the violation |
|---|---|---|---|
| Required field | @nullable | value is not null | cannot-be-null |
| Required only on create | @notNullOnCreate | as above, only on create | cannot-be-null |
| Required only on update | @notNullOnUpdate | as above, only on PUT and PATCH | cannot-be-null |
| not empty | @notEmpty | text not empty and not only spaces, list with at least one entry | cannot-be-empty |
| Length (property of the field) | – | text has at most this many characters, default 255 | too-long |
| Pattern | @pattern | the whole text matches the regular expression | pattern-mismatch |
| Minimum value | @min | number ≥ limit | min-number |
| Maximum value | @max | number ≤ limit | max-number |
| in the future | @future, @futureOrPresent | date or point in time after now (or from now on) | not-in-future |
| in the past | @past, @pastOrPresent | date or point in time before now (or up to now) | not-in-past |
| unique | @unique | the database checks it on save | – (own response, see below) |
A few details that matter in daily work:
@nullablemeans required field, even though the name suggests the opposite.- Each rule only checks its own value type: length and pattern only texts, minimum and maximum value only numbers. Limits are inclusive and may have decimals (
10.5). - For a plain date (
LocalDate), CDMS compares with today. Today is neither future nor past. That is what@futureOrPresentand@pastOrPresentare for. - A
nullvalue only breaks the required rules. “Not empty”, length, pattern and limits only check values that are present. - You set the time rules in the model’s metadata (YAML). The hub UI does not offer them.
When which rule applies: the scope
You can limit many rules to a scope:
| ALWAYS | CREATE | UPDATE | |
|---|---|---|---|
| applies on create | yes | yes | no |
| applies on PUT | yes | no | yes |
| applies on PATCH | yes, for sent fields | no | yes, for sent fields |
These rules have a scope: “not empty”, pattern, minimum and maximum value (the two share one), and the time rules. For the required field, the operation is part of the rule ID itself: @nullable always applies, @notNullOnCreate and @notNullOnUpdate only on one operation. The length from the field property always applies.
What counts is the operation per object, not the endpoint. If you send a new employee without an id in a PATCH on a company, this employee is created and checked with the CREATE rules.
What gets checked: create, PUT, PATCH
- every field of the model
- a missing field is
null - default values are already filled in, so a field with a default meets the requirement
- every field of the model
- a missing field is
null, because PUT clears it - a missing list counts as
null
- only the sent fields
- a missing field stays as it is and is not checked
- a sent
nullis checked
This explains a common difference: a PATCH with a single field goes through. The same content as a PUT fails with cannot-be-null on every required field you did not send. See PUT or PATCH? The null trap
The flow: collect, hooks, check
-
1Client→CDMSsends the object with four entries in
members -
2CDMSgoes through every field, also in the child objects, and remembers each violation instead of stopping right away
-
3Hookbefore hooks run and may still change fields, for example fill a required field
-
4CDMSchecks each remembered violation again against the value that is now on the object
-
5CDMS→Clientviolations are left → 422 with all of them at once, nothing is saved
-
6CDMS→Databaseno violations → saves, then the after hooks run
So a before hook can resolve a violation, for example by setting a missing required field itself. After hooks only run after the check, and CDMS does not check their changes anymore. More about hooks in Hooks: types and timing.
What a 422 looks like
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" }
]
}This is how you read the path in field:
| Path | means |
|---|---|
name | field of the object itself |
address.street | field street in the single reference address |
members[1].name | field name in the second entry of the list members (counted from 0) |
lists[2].groups[0].name | nested as deep as needed |
The index is the position in the list as you sent it. If a field has two violations, there are two entries in violations. How a client turns this into marked form fields is described in Getting validation errors into the form.
Child objects
CDMS also checks child objects when it writes them:
- A new child without an
idis created and checked with the CREATE rules of its model. - A child with an
idthat is changed along with the parent through the relationship is checked with the UPDATE rules. With PATCH, only the sent fields. - A child that is only linked by
idis not checked by CDMS. After all, it is not changed.
If a child fails, the valid parent is not saved either. The whole request is one transaction. Which relationship may create or change children is described in The four cases in nested writing.
Unique: the database checks
The “unique” rule (@unique) becomes a unique index in the database. CDMS does not check it beforehand. The database checks it on save. That is why the response looks different:
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 checks before saving
- all violations at once
violationswith field path and rule
already-exists- the database rejects on save
- one error, without a field reference
- no
violations
Decision table
| Values in the JSON have the right type | all rules met (after the before hooks) | Database accepts the row | Response |
|---|---|---|---|
| no | – | – | 400 invalid-value|<pfad>, before the rules are checked |
| yes | no | – | 422 validation-failed with all violations |
| yes | yes | no, value already exists | 409 already-exists |
| yes | yes | yes | 200 |
“Right type” means: text in a text field, a number in a number field, points in time as yyyy-MM-dd HH:mm:ss, an enum value that the model knows. If a value does not fit, CDMS does not check the rules at all. It responds with 400 invalid-value and the path of the field:
| Request | Response |
|---|---|
POST /create with "amount": "sieben" | 400 invalid-value|data.amount |
PATCH /update/{id} with "dueOn": "10.02.2026" | 400 invalid-value|dueOn |
| JSON that cannot be read at all | 400 malformed-json |
body without data | 400 missing-data |
When Create and PUT read the body, the path starts at the root of the body (data.amount). With PATCH, it starts inside data (dueOn), like in violations.
Pitfalls
Where to go next
- Why PUT checks other fields than PATCH: PUT or PATCH? The null trap
- Fields that get a value on create: Default values
- Setting rules in the hub: Modeling in the hub
- The error format at a glance: The response format:
dataandmeta