Skip to content
errium

Validation normalization

How nested Pydantic errors become a flat, readable map.

ValidationNormalizer.normalize(errors) takes a raw Pydantic or FastAPI errors() list — dicts carrying loc, type and msg — and returns a flat {field_path: message} dict.

{
  "detail": [
    { "type": "missing", "loc": ["body", "password"], "msg": "Field required", "input": null }
  ]
}

becomes:

{
  "password": "Password is required."
}

The three steps

1. Strip the location prefix. If the first loc segment is one of body, query, header, path or formData, it is dropped. The remaining segments join with dots, so a nested field becomes address.postcode. An empty path falls back to the key field.

2. Look up a message template. The normalizer tries, in order: the error’s type, then its msg lowercased, then msg as given. If none match, it scans for a mapping key contained in either the message or the type. If nothing matches at all, the original message is used unchanged.

The built-in mappings:

KeyTemplate
missing{field_name} is required.
field required{field_name} is required.
value_error.emailInvalid email format.
value is not a valid emailInvalid email format.
none is not an allowed valueValue cannot be empty.

3. Format and tidy. A {field_name} placeholder is filled with the last path segment, underscores replaced by spaces and the first letter capitalised — so user_id renders as “User id”. The result gets a trailing full stop if it lacks one, and its first character capitalised.

Custom mappings

Pass your own templates at construction. They merge over the defaults, so you can override a built-in or add a new type:

from errium_core.normalizers.validation import ValidationNormalizer

normalizer = ValidationNormalizer(
    custom_mappings={
        "string_too_short": "{field_name} is too short.",
        "missing": "We need a {field_name} to continue.",
    }
)

Which adapters use it

The FastAPI validation handler, the Flask extension, and the Ninja adapter all run ValidationNormalizer. A Django ValidationError raised on Ninja or DRF is flattened too: {field: "message; message"}, with _ for non-field errors.

details is capped at 50 entries; see The response contract.