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:
Invalid component annotations, like
parsed_body: Body[Model] | None, see Default valuesOr invalid
HttpSpecusage
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.
1from http import HTTPStatus
2
3from dmr import Controller, modify
4from dmr.plugins.pydantic import PydanticFastSerializer
5
6
7class JobController(Controller[PydanticFastSerializer]):
8 @modify(status_code=HTTPStatus.NO_CONTENT)
9 def post(self) -> None:
10 print('Job created') # noqa: WPS421
11
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
1from http import HTTPStatus
2
3from dmr import Controller, modify
4from dmr.plugins.pydantic import PydanticFastSerializer
5from dmr.settings import HttpSpec
6
7
8class JobController(Controller[PydanticFastSerializer]):
9 @modify(
10 status_code=HTTPStatus.NO_CONTENT,
11 no_validate_http_spec={HttpSpec.empty_response_body},
12 )
13 def post(self) -> int:
14 job_id = 4 # very random number :)
15 return job_id # noqa: RET504
16
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: 1
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
1from http import HTTPStatus
2
3from dmr import Controller, modify
4from dmr.plugins.pydantic import PydanticFastSerializer
5from dmr.settings import HttpSpec
6
7
8class JobController(Controller[PydanticFastSerializer]):
9 no_validate_http_spec = frozenset((HttpSpec.empty_response_body,))
10
11 @modify(status_code=HTTPStatus.NO_CONTENT)
12 def post(self) -> int:
13 job_id = 4 # very random number :)
14 return job_id # noqa: RET504
15
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:04 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 1
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
Controllerclasses have unique methodsThat 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#
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_schemaflag 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_responsesflag 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
EMPTYis not explicit:None, empty collections, and all other values are used as-is. Override this class and set it asmetadata_merger_clsto change this. For example, to mergeauthfrom all layers:Added in version 0.16.0.
- empty_to_none(layer: _LayerT | EMPTY) _LayerT | None[source]#
Convert
EMPTYtoNonefor the resolved metadata.Resolved metadata does not have the “not set” state anymore, so
Noneis 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
EMPTYis not explicit, the next layer is used instead of it. It returnsEMPTYif no layer has an explicit value.Noneis 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, whereFalse,0, andNoneare real values. It returnsEMPTYif all layers areEMPTY.
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
HttpResponsethat 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
HttpResponseBaseobject directly. RemovedValidatedModificationcreation.
- 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.