CodamAIDocs
Topicdone

Getting validation errors into the form

How a client maps the collected violations of a 422 to the form fields, including nested paths.

Variants
simple fieldfield in a single referencefield in a listfield the form does not showunknown rule400 invalid-value with patherrors without a fieldthrough a BFF

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

Creating a customer with address and contacts
Response 422
{
  "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" }
  ],
  …
}
Form afterwards
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

From submitting to marking
  1. 1
    User→Frontend
    clicks "Save"
  2. 2
    Frontend
    removes old marks and disables the button
  3. 3
    Frontend→CDMS
    POST /api/rest/crm/customer/create with the form content in data
  4. 4
    CDMS
    checks all fields, also in references and lists, and collects every violation
  5. 5
    CDMS→Frontend
    422 validation-failed with all violations, nothing was stored
  6. 6
    Frontend
    maps each entry to an input field by field and translates rule into a text
  7. 7
    Frontend→User
    shows 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:

fieldwhat it meansinput field in the form
namefield of the object itself“Name”
address.zipfield zip in the single reference address“ZIP” in the address block
contacts[1].emailfield email in the second entry of the list contacts, counted from 0“Email” in row 2
contactsthe list itself, e.g. with cannot-be-emptythe whole contacts block
lists[2].groups[0].namenested to any depththe 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

rulesuggested text
cannot-be-nullis required
cannot-be-emptymust not be empty
too-longis too long
pattern-mismatchdoes not have the allowed format
min-numberis smaller than allowed
max-numberis larger than allowed
not-in-futuremust be in the future
not-in-pastmust 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

Grouping violations by field
Mapping
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;
}
Using it
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

What happens with which entry

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.

Where does each error go?
StatusmessageKeyfield referenceDisplay
422validation-failedviolationsmark each field, hint above the form
400invalid-value|<path>path in the keymark this one field: "has the wrong type"
409already-existsnonemessage above the form, e.g. "value is already taken". If you know the unique field, mark it yourself.
422chosen by the hook, layer: hooknonemessage above the form, text based on the hook's messageKey
422missing-attribute-on-profile|…nonenot 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

Sources in the code and the knowledge base
  • CDMS/cdms-rest-api – CdmsExceptionMapper (violations with field and rule), ClientErrorTranslator (invalid-value|<path>)
  • CDMS/cdms-system-layer – AbstractLayer.violatedRules, assertValid; ValidationRequestContext.pathOf
  • commons – FieldViolation, TemporalRule (not-in-future, not-in-past)
  • CDMS/frontend – shared/utils/cdmsViolations.ts (parseViolations, violationsByField), app/utils/cdms/apiError.ts (toApiErrorInfo), server/utils/useCmsApi.ts
  • documentation/05-api-guide/10-fehler.md, 60-erweiterung/02-validierung.md
Search