What this is about
When a form sends data that breaks field rules, CDMS answers with 422 and a list violations. Each entry names the field with its path and the violated rule. Your job in the client: map each entry to the matching input field and show an understandable message there.
From the 422 to the marked form
{
"messageKey": "validation-failed",
"code": "422",
"violations": [
{ "field": "name",
"rule": "cannot-be-empty" },
{ "field": "address.zip",
"rule": "pattern-mismatch" },
{ "field": "contacts[1].email",
"rule": "cannot-be-null" }
],
…
}Create customer
─────────────────────────────────────────
Name [ ]
⚠ must not be empty
Address
Street [ 1 Main Street ]
ZIP [ 123 ]
⚠ does not have the allowed format
Contacts
1 Anna [ anna@muster.de ]
2 Ben [ ]
⚠ is required
─────────────────────────────────────────
Please correct the marked fields.The flow
-
1User→Frontendclicks "Save"
-
2Frontendremoves old marks and disables the button
-
3Frontend→CDMS
POST /api/rest/crm/customer/createwith the form content indata -
4CDMSchecks all fields, also in references and lists, and collects every violation
-
5CDMS→Frontend422
validation-failedwith all violations, nothing was stored -
6Frontendmaps each entry to an input field by
fieldand translatesruleinto a text -
7Frontend→Usershows the messages at the fields, plus a hint above the form
Reading the path
The path in field describes where the field sits in the object you sent. It starts inside data:
field | what it means | input field in the form |
|---|---|---|
name | field of the object itself | “Name” |
address.zip | field zip in the single reference address | “ZIP” in the address block |
contacts[1].email | field email in the second entry of the list contacts, counted from 0 | “Email” in row 2 |
contacts | the list itself, e.g. with cannot-be-empty | the whole contacts block |
lists[2].groups[0].name | nested to any depth | the field in the matching sub-row |
It is easiest if your form fields carry the same path as their name, for example name="contacts[1].email". Then field is directly the key under which you store the message.
The index in […] is the position in the list as you sent it. If the form shows the rows in the same order, the message hits the right row.
Translating the rules into text
rule | suggested text |
|---|---|
cannot-be-null | is required |
cannot-be-empty | must not be empty |
too-long | is too long |
pattern-mismatch | does not have the allowed format |
min-number | is smaller than allowed |
max-number | is larger than allowed |
not-in-future | must be in the future |
not-in-past | must be in the past |
The response does not contain the limits themselves, such as the allowed length or the minimum value. If you want to show them in the text, take them from your model. Which rule checks what is described in Validation.
An example in TypeScript
type Violation = { field: string; rule: string };
const TEXT: Record<string, string> = {
'cannot-be-null': 'is required',
'cannot-be-empty': 'must not be empty',
'too-long': 'is too long',
'pattern-mismatch': 'does not have the allowed format',
'min-number': 'is smaller than allowed',
'max-number': 'is larger than allowed',
'not-in-future': 'must be in the future',
'not-in-past': 'must be in the past',
};
function byField(violations: Violation[]) {
const errors: Record<string, string[]> = {};
for (const v of violations) {
const text = TEXT[v.rule] ?? `is invalid (${v.rule})`;
(errors[v.field] ??= []).push(text);
}
return errors;
}const res = await fetch(url, { method: 'POST', … });
if (res.status === 422) {
const body = await res.json();
const errors = byField(body.violations ?? []);
for (const [path, texts] of Object.entries(errors)) {
const input = form.querySelector(`[name="${path}"]`);
if (input) markField(input, texts);
else showFormMessage(`${path} ${texts.join(', ')}`);
}
}The example does two things on purpose:
- An unknown rule gets a general text instead of a crash. That keeps your client stable when a rule is added.
- A field the form does not show ends up in the message above the form. This happens, for example, with a PUT when a required field is not in the form and therefore arrives as
null.
All variants
When: field is a single name, e.g. name
The input field with this name gets the message.
Result: The most common case.
When: field contains a dot, e.g. address.zip
The field sits in a sub-object. If the form shows the address as its own block, mark the field zip there.
Result: Path and form structure match if the blocks are named like the references.
When: field contains an index, e.g. contacts[1].email
Take the row at position 1, counted from 0, and mark email there. Prerequisite: the form shows the rows in the order in which you sent them.
Result: For tables with rows to add and remove, do not re-sort while messages are visible.
When: There is no input field for field
Show the message above the form, with the field name. Often a required field is then missing from the form itself.
Result: No violation gets lost.
When: rule is not in your text table
Show a general text such as "is invalid" and name the rule in brackets.
Result: The client stays stable when rules are added.
When: The frontend does not talk to CDMS directly, but through its own server
The BFF passes status and violations on to the frontend unchanged. The CDMS portal does it like this: the server puts the list into its own error response, the form marks the fields from it and shows "Please correct the marked fields." at the top.
Result: If the BFF already turns the list into a single text, the form can no longer mark fields.
Other errors with a field
It is not only 422 that can concern a field. A value that does not even fit the field type, such as text in a number field, is refused too: with 400 invalid-value|<path>. This comes before the rules are checked, so on its own and without violations.
Here the path is in the messageKey after the |. For create and PUT it starts at the root of the body (data.amount), for PATCH inside data (amount). Cut off a leading data., then it matches your form fields.
| Status | messageKey | field reference | Display |
|---|---|---|---|
| 422 | validation-failed | violations | mark each field, hint above the form |
| 400 | invalid-value|<path> | path in the key | mark this one field: "has the wrong type" |
| 409 | already-exists | none | message above the form, e.g. "value is already taken". If you know the unique field, mark it yourself. |
| 422 | chosen by the hook, layer: hook | none | message above the form, text based on the hook's messageKey |
| 422 | missing-attribute-on-profile|… | none | not a form error: the person's profile lacks an attribute. Show a general message. |
| other | – | – | general message, see Map of status codes |
Pitfalls
Where to go next
- Which rules exist and when they apply: Validation
- Why PUT reports other fields than PATCH: PUT or PATCH? The null trap
- How every error response is built: The error format