Skip to content

Error and Validation Contract ​

This page describes the error bodies that VUEDA's server sends and the client error classes that parse them. It covers the 400 validation body, the 409 body of Warning Confirmation, and the class that each CRUD Adapter throws for each status. Form State and Validation Lifecycle describes how a form stores these errors and runs the confirmation. Handle Form Validation and Server Errors and Require Confirmation Before a Write give the steps.

mermaid Diagram
How a write's validation error or warning travels from the server to form state

Boundary and Authority ​

The server decides what is valid, what is a warning, and the shape of each error body. The client decides how it stores and shows those bodies. The two sides share only the HTTP status and the JSON body.

A write adapter reads the status first. A 400 carries validation feedback, and a 409 carries warnings that need confirmation. Any other failure is a generic error that never enters form state. Read requests follow different rules, which Client Classification and Form-State Ingestion lists.

Wire Error Shapes and Status Branches ​

The generated REST pages do not declare the validation 400 body or the 409 confirmation body (#376, #145). Some of their error examples do not match what the server sends (#238). This section describes the bodies that the server sends.

The validation shape ​

VUEDA's validation code raises VuedaValidationError, a subclass of DRF: ValidationError. A field-keyed error is a JSON object whose keys are field names, such as {"name": ["Too long."]}. The value is usually a list of messages, and a plain string also reaches the client under its field name. An error raised with a single message or a list becomes a Non-Field Error.

core.exceptions.debug_stack_exception_handler shapes every error body before the server sends it:

  • A validation error whose detail is a list moves under the non_field_errors key: {"non_field_errors": ["..."]}.
  • Every error body gets a serverStack string. Under DEBUG, or when the IN_TESTS setting is true, it holds the full traceback. Otherwise it holds the exception class and message.
  • An exception that DRF does not handle becomes a 500 with a detail message and serverStack.

The 409 confirmation body is the one response without serverStack. The handler also marks the request's transaction for rollback before it answers, as Configuration Surface and Defaults describes.

A 400 that is not a validation error carries a detail string: {"detail": "..."}. Malformed JSON produces one, and so does BadRequestException.

Unknown input fields ​

NoExtraFieldsSerializerMixin, part of VuedaSerializer, rejects a write body key that the view's serializer does not declare. Each unknown key gets its own entry with the message Invalid field. Valid fields are .... The mixin checks a key such as items[0]quantity or items.quantity by its base name, items. It does not check keys inside a nested serializer's payload. It always accepts formatted_name on a model that has one.

On list and retrieve, the server rejects unknown query parameters, as Filtering and Ordering Semantics describes (Query Parameter Validation).

An invalid f or e name returns the same shape with serverStack, on a read and on a write. Each invalid name is its own key, such as "amount": ["Invalid field. Valid fields are ..."]. One response reports the invalid names from both parameters. A write checks the names before it validates the body, so it saves nothing. Field and Expand Semantics describes which names each action accepts.

Other error statuses ​

A 403 or 404 from a VUEDA view carries {"detail": "..."} plus serverStack. These statuses never carry validation feedback. For example, the choices endpoints answer 404 for an unknown model, field, or filter, and 403 when permission is denied, as ModelInfoChoicesViewSet and ModelInfoFilterSetChoicesViewSet describe. The client raises a ModelChoicesError for these statuses. That error never enters form state.

Non-Field and Nested Path Semantics ​

FormValidationError turns a 400 body into a map from Field Path to a list of entries. It removes serverStack from the body first and keeps it as its own serverStack property. It then flattens the body into paths and drops a trailing list index from each path:

400 bodyKey in errors
{"name": ["Too long."]}name
{"name": "Too long."}name
{"address": {"city": ["Required."]}}address.city
{"items": [{"quantity": ["Too large."]}]}items[0].quantity
{"non_field_errors": ["Already exists."]}non_field_errors
{"detail": "Malformed request."}detail
{"non_field_errors": [{"detail": "...", ...}]}non_field_errors
{"non_field_errors": [{"rows": [1, 2]}]}non_field_errors[0].rows

The client does not map these paths onto the form's field tree. A key matches a field only when it equals that field's path. The client mirrors DRF's non-field key as utils/constants.NON_FIELD_ERRORS_KEY, and FormMessage reads that key for form-level errors.

An entry object with a detail key is a structured feedback object. It stays whole under the path that holds it, so its other properties reach the renderer with it. An object without a detail key splits into one path per value, as the last table row shows. Handle Form Validation and Server Errors shows how to render structured objects.

A key can name something other than a field. A bulk delete answers {"9": ["Object with pk=9 does not exist."]} for a pk that is missing or that the user may not delete.

Warning Confirmation Semantics ​

Warnings are advisory. The server withholds a write that has unacknowledged warnings and answers 409 Conflict with this body:

json
{
    "confirmation_required": true,
    "digest": "3f2a9c1e0b7d4a61",
    "warnings": { "quantity": ["Exceeds usual order size."] }
}

core.exceptions.gate_warnings produces this answer. It computes the digest from the warnings and compares it with the request's Acknowledge-Warnings header (core.exceptions.ACKNOWLEDGE_WARNINGS_HEADER). A match lets the write proceed, and anything else returns the 409 with nothing written. Require Confirmation Before a Write describes the server hooks that produce warnings.

The warnings mapping has one of two shapes, chosen by the request path:

  • Aggregate, {field: [messages]}, for a request on one object. Create, update, and partial update use it, as do the detail routes of destroy, activate, deactivate, and transitions. A warning not tied to a field uses the non_field_errors key.
  • Per-object, {object_id: {field: [messages]}}, for a bulk request. Each warned object gets one entry keyed by str(pk). WarningConfirmationMixin builds it for bulk destroy, activate, and deactivate, and the workflow viewset builds it for a transition on several objects.

A bulk request that affects one object still gets the per-object shape. An action declared with confirm=True always sends the aggregate shape, with its message under non_field_errors. A custom action that calls gate_warnings chooses its own shape, because gate_warnings checks only that the mapping is not empty. The client's default rendering understands only these two shapes.

The client raises ConfirmationRequiredError for a 409. It holds the warnings mapping as messages, the digest as digest, and an empty errors map. The body does not say which shape messages has, so the calling adapter sets bulk. The bulk adapters set it to true and the object adapters to false. storeWorkflow.executeTransition sets it to true when the caller passes a list of pks. FormConfirmDialog passes bulk to its warnings slot, where ModelActionForm uses it to group warnings by object. Form State and Validation Lifecycle describes the confirm-and-resubmit flow.

Client Classification and Form-State Ingestion ​

Each default adapter maps a failed response to an error class:

Call400409Other failure
defaultObjectCreate, defaultObjectUpdate, defaultObjectPatchFormValidationErrorConfirmationRequiredError, bulk falseFetchError
defaultObjectDelete, defaultObjectExecuteActionFormValidationErrorConfirmationRequiredError, bulk falseFetchError
defaultObjectsDelete, defaultListExecuteActionFormValidationErrorConfirmationRequiredError, bulk trueFetchError
storeWorkflow.executeTransitionFormValidationErrorConfirmationRequiredError, bulk true for a list of pksWorkflowError
storeUser sign-in, password, and two-factor actionsFormValidationErrora FetchError subclassUnauthorizedError for 401 or 403, otherwise a FetchError subclass
defaultObjectRetrieveFetchErrorFetchErrorFetchError
singlePagePaginatedListCrudAdaptor, allPagePaginatedListCrudAdaptorListFilterError or FetchErroras 400as 400

The list adapters raise ListFilterError when the error body has a key named after a query parameter that the request sent, and FetchError otherwise. CRUD Adapter Layer lists the parameters that this check leaves out. In storeUser, the reset-link check is the one call whose 400 is not form feedback: it raises InvalidResetPasswordLinkError.

FormValidationError and ConfirmationRequiredError both extend ServerFeedbackError. That class carries two maps keyed by field path: errors for blocking feedback and messages for warnings. FormValidationError fills only errors, and ConfirmationRequiredError fills only messages. handleServerFormValidationError writes both maps into the Form Context as Server Feedback.

Form code branches on the class first. A ConfirmationRequiredError with a digest starts the confirmation, and any other ServerFeedbackError becomes field and form errors. A replacement adapter can throw its own ServerFeedbackError subclass, and forms store its maps the same way. An error of any other class stays out of form state.

Observable Failure Signatures ​

A key that no component shows. A detail key, a pk key, or a path that matches no rendered field has no field to show it. ActionForm lists these errors in its validation summary. The object form views, ViewCreate and ViewUpdate, have no such summary, so the user sees only the save failure toast.

Non-object bodies. FormValidationError spreads the body into an object. A string body becomes one entry per character, keyed 0, 1, and so on, and an array body becomes one entry per item. VUEDA's handler always sends an object, so these bodies come from a proxy or a non-VUEDA view.

Read errors skip form state. A 400 on list or retrieve is a ListFilterError or a FetchError, never a FormValidationError. Code that catches only FormValidationError misses it.

A 409 without a digest. Form code starts the confirmation only when the error carries a digest. A custom exception handler that drops the digest turns the 409 into an ordinary failure, because there is nothing to acknowledge.