CodamAIDocs
Topicdone

Validation

Which rules are attached to fields, when they are checked, that all violations come collected in one 422, and what the path of a nested error looks like.

Variants
required field on create/updatenot emptylengthpatternmin/maxfuture/pastscope ALWAYS/CREATE/UPDATEpatch only checks what was sentchild objectsunique (database) → 409wrong type in JSON → 400 invalid-value

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 hubRule IDchecksrule in the violation
Required field@nullablevalue is not nullcannot-be-null
Required only on create@notNullOnCreateas above, only on createcannot-be-null
Required only on update@notNullOnUpdateas above, only on PUT and PATCHcannot-be-null
not empty@notEmptytext not empty and not only spaces, list with at least one entrycannot-be-empty
Length (property of the field)–text has at most this many characters, default 255too-long
Pattern@patternthe whole text matches the regular expressionpattern-mismatch
Minimum value@minnumber ≥ limitmin-number
Maximum value@maxnumber ≤ limitmax-number
in the future@future, @futureOrPresentdate or point in time after now (or from now on)not-in-future
in the past@past, @pastOrPresentdate or point in time before now (or up to now)not-in-past
unique@uniquethe database checks it on save– (own response, see below)

A few details that matter in daily work:

  • @nullable means 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 @futureOrPresent and @pastOrPresent are for.
  • A null value 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:

Scope of a rule
ALWAYSCREATEUPDATE
applies on createyesyesno
applies on PUTyesnoyes
applies on PATCHyes, for sent fieldsnoyes, 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

Which fields validation looks at
POST /create
rules with scope ALWAYS and CREATE
  • 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
PUT /update/{id}
rules with scope ALWAYS and UPDATE
  • every field of the model
  • a missing field is null, because PUT clears it
  • a missing list counts as null
PATCH /update/{id}
rules with scope ALWAYS and UPDATE
  • only the sent fields
  • a missing field stays as it is and is not checked
  • a sent null is 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

Validation on POST /api/rest/roster/create
  1. 1
    Client→CDMS
    sends the object with four entries in members
  2. 2
    CDMS
    goes through every field, also in the child objects, and remembers each violation instead of stopping right away
  3. 3
    Hook
    before hooks run and may still change fields, for example fill a required field
  4. 4
    CDMS
    checks each remembered violation again against the value that is now on the object
  5. 5
    CDMS→Client
    violations are left → 422 with all of them at once, nothing is saved
  6. 6
    CDMS→Database
    no 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

Two violations in one list
Request
POST /api/rest/roster/create
{
  "data": {
    "name": "Frühschicht",
    "members": [
      { "name": "Erste" },
      { "name": "" },
      { "name": "Dritte" },
      { }
    ]
  },
  "response": ["id"]
}
Response 422
{
  "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:

Pathmeans
namefield of the object itself
address.streetfield street in the single reference address
members[1].namefield name in the second entry of the list members (counted from 0)
lists[2].groups[0].namenested 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 id is created and checked with the CREATE rules of its model.
  • A child with an id that 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 id is 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:

Second customer with the same customer number
Request
POST /api/rest/crm/customer/create
{ "data": { "number": "K-1001", "name": "Zweite GmbH" }, "response": ["id"] }
Response 409
{
  "error": "DataIntegrityException",
  "messageKey": "already-exists",
  "code": "409",
  "layer": "database"
}
Rule violation or database violation?
Rule violated
422 validation-failed
  • CDMS checks before saving
  • all violations at once
  • violations with field path and rule
Uniqueness violated
409 already-exists
  • the database rejects on save
  • one error, without a field reference
  • no violations

Decision table

Response to a write with invalid content
Values in the JSON have the right typeall rules met (after the before hooks)Database accepts the rowResponse
no––400 invalid-value|<pfad>, before the rules are checked
yesno–422 validation-failed with all violations
yesyesno, value already exists409 already-exists
yesyesyes200

“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:

RequestResponse
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 all400 malformed-json
body without data400 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

Sources in the code and the knowledge base
  • CDMS/cdms-system-layer – AbstractLayer.validateField, violatedRules, assertValid; ValidationRequestContext (pathOf, recheck); AbstractSystemLayer.createObject/updateObject/patchObject
  • commons – MetaFieldRules (appliesTo), RuleScope, TemporalRule, FieldViolation
  • CDMS/cdms-generator – CdmsYamlLoader.applyRuleList, normalizeScope; EntityProcessor (@Column unique, updatable)
  • CDMS/cdms-persistence-database – AbstractDatabasePersistence, PersistenceFailureTranslator (DataIntegrityException 409 already-exists)
  • CDMS/cdms-rest-api – CdmsExceptionMapper, payloads/WritePayload (ADR-009)
  • CDMS/cdms-integrationtest – AbstractFieldRulesTest, AbstractTemporalRulesTest, AbstractDatabaseRulesTest
  • documentation/60-erweiterung/02-validierung.md
Search