Validation#

django-modern-rest has several layers of import-time and runtime validation. We try to do everything that we can during import-time, so it won’t affect your requests and responses.

However, we have to validate requests and responses during the runtime. Responses validation can be turned off in production for speed.

What do we validate and how?

Settings validation#

We start with settings validation. We only validate settings once per application, we do it when the first Controller is created.

We also validate our own default values to be correct.

See SettingsValidator for the API.

Endpoint validation#

Next, when controller is being created, we run Endpoint validation.

Here we can detect all kinds of problems with how endpoints are defined:

See EndpointMetadataBuilder and EndpointMetadataBuilder for the API.

HttpSpec validation#

You can customize the strictness of HTTP Spec validation with overriding disabled HttpSpec options per-endpoint, per-controller, and globally.

Warning

We don’t recommend overriding any of these settings by default. It only makes sense to change, when implementing some old legacy API the “same” way as it used to be.

And only when you need this for a very specific reason.

Run result

$ curl http://127.0.0.1:8000/api/job/ -D - -X POST
HTTP/1.1 204 No Content
date: Mon, 05 Oct 2026 11:19:03 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

Controller validation#

The last step is the final Controller validation which has everything ready:

Here we validate:

  • That all Controller classes have unique methods

  • That all endpoints are either sync or async

  • All per-controller and per-endpoint error handling

See ControllerValidator for the API.

Response validation#

The last step is to validate the response when returning it from the endpoint in runtime. We need this to make sure that API responses always match response schemas.

It can be turned off.

See ResponseValidator for the API.

API Reference#

Settings#

class dmr.validation.settings.SettingsValidator(*, serializer: type[BaseSerializer])[source]#

Validates defined settings once.

__call__() → None[source]#

Collect and validate settings.

Endpoint#

class dmr.validation.endpoint_metadata.EndpointMetadataBuilder(*, payload: ValidateEndpointPayload | ModifyEndpointPayload | None, controller_cls: type[Controller[BaseSerializer]], func: Callable[[...], Any], metadata_cls: type[EndpointMetadata], response_modification_cls: type[ResponseModification], metadata_merger_cls: type[MetadataMerger], component_parsers: list[ComponentParserSpec], type_annotations: dict[str, Any])[source]#

Validate the metadata definition.

It is done during import-time only once, so it can be not blazing fast. It is better to be precise here than to be fast.

Here we only do structure and required validation. All semantic validation will be performed later on.

Metadata will NOT be considered ready after running this process.

__call__() → EndpointMetadata[source]#

Do the validation.

build_semantic_schema() → bool[source]#

Resolve the semantic_schema flag for this endpoint.

It is public, because other flags can default to this one.

Added in version 0.16.0.

build_validate_responses() → bool[source]#

Resolve the validate_responses flag for this endpoint.

It is public, because other flags can default to this one.

Added in version 0.16.0.

merger(field_name: str) → MetadataMerger[source]#

Create a merger for the given metadata field.

Use it to resolve endpoint, controller, and settings layers the same way built-in fields are resolved.

Added in version 0.16.0.

class dmr.validation.endpoint_metadata.EndpointMetadataValidator(*, metadata: EndpointMetadata, metadata_merger_cls: type[MetadataMerger])[source]#

Builds responses for the endpoint metadata.

Runs semantic validation.

Metadata will be considered ready after running this process.

__call__(func: Callable[[...], Any], payload: ValidateEndpointPayload | ModifyEndpointPayload | None, *, controller_cls: type[Controller[BaseSerializer]]) → None[source]#

Collect and validate all responses.

response_list_validator_cls#

alias of _ResponseListValidator

class dmr.validation.metadata_merger.MetadataMerger(*, field_name: str)[source]#

Define how endpoint, controller, and settings values are resolved.

Every configurable field of the endpoint metadata is resolved with a single call to one of these methods. All layers are passed at once, from the most specific one (endpoint) to the least specific one (settings), so field_name can be used to change how a specific field is resolved.

By default, nothing is merged: the first layer with an explicit value wins. Only EMPTY is not explicit: None, empty collections, and all other values are used as-is. Override this class and set it as metadata_merger_cls to change this. For example, to merge auth from all layers:

Added in version 0.16.0.

empty_to_none(layer: _LayerT | EMPTY) → _LayerT | None[source]#

Convert EMPTY to None for the resolved metadata.

Resolved metadata does not have the “not set” state anymore, so None is used there for missing optional values. It is used for endpoint-only fields, which have a single layer.

first_defined(*layers: _LayerT | EMPTY | None) → _LayerT | EMPTY | None[source]#

Return the first explicitly defined configuration layer.

It is used for collections. Only EMPTY is not explicit, the next layer is used instead of it. It returns EMPTY if no layer has an explicit value.

None is an explicit value, it disables all less specific layers. Empty collections like [] and () are explicit values too, they are taken literally: “nothing on this layer”, they also disable all less specific layers.

first_set(*layers: _LayerT | EMPTY) → _LayerT | EMPTY[source]#

Return the first configuration layer that is not EMPTY.

Unlike first_defined(), it is used for scalar values, where False, 0, and None are real values. It returns EMPTY if all layers are EMPTY.

not_empty(to_check: _LayerT | EMPTY) → _LayerT[source]#

Check that a value is not empty.

Controller#

class dmr.validation.controller.ControllerValidator[source]#

Validates that controller is created correctly.

__call__(controller: type[Controller[BaseSerializer]]) → bool | None[source]#

Run the validation.

Response#

class dmr.validation.response.ResponseValidator(metadata: EndpointMetadata, serializer: type[BaseSerializer])[source]#

Response validator.

Can validate responses that return raw data as well as real HttpResponse that are returned from endpoints.

validate_modification(endpoint: Endpoint, controller: Controller[BaseSerializer], structured: Any) → HttpResponseBase[source]#

Validate structured data before dumping it to json.

Changed in version 0.16.0: Now returns HttpResponseBase object directly. Removed ValidatedModification creation.

validate_response(endpoint: Endpoint, controller: Controller[BaseSerializer], response: _ResponseT) → _ResponseT[source]#

Validate response based on provided schema.

Changed in version 0.15.0: Respects exclude_validate_responses.