Reusable code#
One of the worst things about the current generation of Python REST frameworks is code re-usability.
django-rest-frameworkis very flexible, but all the flexibility comes from importing fully qualified object’s path strings taken from app’s settings. It is very hard to properly type a code base like this. Using it is also really hard, because you can’t easily navigate in your source code.fastapidoes not even offer a way to write reusable code, because it is based on functions, which are really hard to reuse and modify. That’s why you have to copy paste lots of code just to, for example, use the most common things such as JWT auth.
What does django-modern-rest offer instead?
Explicitly abstract controllers#
A controller can have an exact serializer and endpoints,
but you might still not want to route it: you only want to reuse it.
Declare is_abstract = True explicitly for this:
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticFastSerializer
3
4
5class MyBaseController(Controller[PydanticFastSerializer]):
6 # It has an exact serializer and an endpoint,
7 # but we only want to reuse it, we don't want to route it:
8 is_abstract = True
9
10 def get(self) -> str:
11 return 'hello from the base controller'
12
13
14class MyController(MyBaseController):
15 """It does not declare `is_abstract`, so it is concrete again."""
16
17 # It serves the same `GET` request as `MyBaseController`,
18 # but this one builds its endpoints and can be routed.
19
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET
"hello from the base controller"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/mycontroller/": {
"get": {
"operationId": "getMyControllerApiMycontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"summary": "It does not declare `is_abstract`, so it is concrete again."
}
}
}
Such a controller does not build any endpoints,
they are only created in a concrete context.
Subclasses that don’t declare is_abstract themselves
are concrete again, even when their base controller is explicitly abstract.
So, MyController from the example above does the same
GET request as its base, but it can be routed.
Added in version 0.16.0.
Providing extras for @modify and @validate#
Your custom controllers might require custom parameters that users can provide
to validate and modify.
To do so, we utilize extras= parameter. By default it is always typed
as empty sentinel, because default controller do not allow any extra parameters.
Three steps are needed:
Define a subclass of
Extras. Its fields can default toEMPTYif some arguments can be missingDefine
buildwith how to build your value from several configuration layers, you can use global settings there as wellCreate typed decorators by passing this class to
ModifyEndpointandValidateEndpointAssign an instance of this class to
extrasattribute of your controller. It enablesextras=for all endpoints of this controller and provides controller-level defaults
build() receives the endpoint layer,
which is EMPTY when extras= is not passed,
and the controller layer as instances of your class,
and returns the resolved value.
It is stored inside extras
and can be read with of() in a typed way.
First, define the extras model itself:
1import dataclasses
2from typing import Self
3
4from typing_extensions import Sentinel, override
5
6from dmr.controller import Controller
7from dmr.endpoint import Extras
8from dmr.serializer import BaseSerializer
9from dmr.types import EMPTY
10from dmr.validation import EndpointMetadataBuilder
11
12
13@dataclasses.dataclass(slots=True, frozen=True, kw_only=True)
14class SmartResponse(Extras[str]):
15 response_text: str | Sentinel = EMPTY
16
17 @classmethod
18 @override
19 def build(
20 cls,
21 from_endpoint: Self | Sentinel,
22 from_controller: Self,
23 controller_cls: type[Controller[BaseSerializer]],
24 builder: EndpointMetadataBuilder,
25 ) -> str:
26 merger = builder.merger('response_text')
27 return merger.not_empty(
28 merger.first_set(
29 (
30 EMPTY
31 if isinstance(from_endpoint, Sentinel)
32 else from_endpoint.response_text
33 ),
34 from_controller.response_text,
35 'default response',
36 ),
37 )
Then define and use new endpoint decorators:
Real world example: dmr.streaming.modify
1from typing import Final
2
3from dmr.controller import Controller
4from dmr.endpoint import ModifyEndpoint
5from dmr.plugins.pydantic import PydanticFastSerializer
6from examples.reusable_code.extras_model import SmartResponse
7
8#: Same as :data:`dmr.modify`, but supports ``extras=SmartResponse(...)``.
9modify: Final = ModifyEndpoint(SmartResponse)
10
11
12class APIController(Controller[PydanticFastSerializer]):
13 extras = SmartResponse()
14
15 def get(self) -> str:
16 return SmartResponse.of(self)
17
18 @modify(extras=SmartResponse(response_text='from endpoint'))
19 def post(self) -> str:
20 return SmartResponse.of(self)
21
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET
"default response"
$ curl http://127.0.0.1:8000/api/example/ -X POST
"from endpoint"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"operationId": "getAPIControllerApiApicontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"post": {
"operationId": "postAPIControllerApiApicontroller",
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "Created"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Real world example: dmr.streaming.validate
1from http import HTTPStatus
2from typing import Final
3
4from django.http import HttpResponse
5
6from dmr import ResponseSpec
7from dmr.controller import Controller
8from dmr.endpoint import ValidateEndpoint
9from dmr.plugins.pydantic import PydanticFastSerializer
10from examples.reusable_code.extras_model import SmartResponse
11
12#: Same as :data:`dmr.validate`, but supports ``extras=SmartResponse(...)``.
13validate: Final = ValidateEndpoint(SmartResponse)
14
15
16class APIController(Controller[PydanticFastSerializer]):
17 extras = SmartResponse(response_text='from controller')
18
19 @validate(
20 ResponseSpec(str, status_code=HTTPStatus.OK),
21 extras=SmartResponse(response_text='from endpoint'),
22 )
23 def get(self) -> HttpResponse:
24 return self.to_response(SmartResponse.of(self))
25
26 @validate(ResponseSpec(str, status_code=HTTPStatus.CREATED))
27 def post(self) -> HttpResponse: # controller level extras
28 return self.to_response(SmartResponse.of(self))
29
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET
"from endpoint"
$ curl http://127.0.0.1:8000/api/example/ -X POST
"from controller"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"operationId": "getAPIControllerApiApicontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"post": {
"operationId": "postAPIControllerApiApicontroller",
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "Created"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
This way your controller subtypes can have any extras that you need!
These definitions would only differ in terms of typing. Everything else would work the same way.
Added in version 0.16.0.
Reusable controllers#
We offer a concept of a “reusable controllers”.
To make a reusable controller, you need
to provide typing.TypeVar instead of a
real BaseSerializer type.
Here’s an example:
1from typing import TypeVar
2
3from typing_extensions import TypedDict
4
5from dmr import Controller
6from dmr.serializer import BaseSerializer
7
8_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
9
10
11class _ResponseBody(TypedDict):
12 message: str
13
14
15class ReusableController(Controller[_SerializerT]):
16 def get(self) -> _ResponseBody:
17 serializer_name = self.serializer.__name__
18 return {'message': f'hello from {serializer_name}'}
This code can work with both pydantic and msgspec as serializers.
Let’s try to create two exact controllers with exact serializers:
1from dmr.plugins.msgspec import MsgspecSerializer
2from examples.reusable_code.reusable_controller import ReusableController
3
4
5class MsgspecController(ReusableController[MsgspecSerializer]):
6 """This controller will use msgspec for serialization."""
7
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET
{"message":"hello from MsgspecSerializer"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"title": "_ResponseBody",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/msgspeccontroller/": {
"get": {
"operationId": "getMsgspecControllerApiMsgspeccontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"summary": "This controller will use msgspec for serialization."
}
}
}
1from dmr.plugins.pydantic import PydanticFastSerializer
2from examples.reusable_code.reusable_controller import ReusableController
3
4
5class PydanticController(ReusableController[PydanticFastSerializer]):
6 """This controller will use pydantic for serialization."""
7
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET
{"message":"hello from PydanticFastSerializer"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"message"
],
"title": "_ResponseBody",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/pydanticcontroller/": {
"get": {
"operationId": "getPydanticControllerApiPydanticcontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "OK"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"summary": "This controller will use pydantic for serialization."
}
}
}
Basically - we just specify what kind of serializer to use. And that’s it. But, this is just the first step. We can do much more!
Note
Only controllers with an exact serializer and at least one endpoint
can be routed. Reusable ones have is_abstract set to True
and raise EndpointMetadataError
when you call .as_view() on them.
Route their subclasses instead.
Added in version 0.16.0.
Tip
Annotate class-level options like responses, auth, parsers,
renderers, and throttling in controllers that will be subclassed.
Without an annotation type-checkers infer a fixed-size tuple from the value you assign, and subclasses won’t be able to add or to remove items from it.
1from collections.abc import Sequence
2from http import HTTPStatus
3from typing import ClassVar
4
5from dmr import Controller, ResponseSpec
6from dmr.errors import ErrorModel
7from dmr.plugins.pydantic import PydanticFastSerializer
8
9
10class MyBaseController(Controller[PydanticFastSerializer]):
11 responses: ClassVar[Sequence[ResponseSpec]] = (
12 ResponseSpec(ErrorModel, status_code=HTTPStatus.NOT_FOUND),
13 )
14
15 def get(self) -> str:
16 return 'reusable'
17
18
19class MyController(MyBaseController):
20 # This works only because `responses` is annotated in the parent class.
21 # Without that annotation type-checkers infer the type of `responses`
22 # from the parent's value, which is a tuple of exactly one item,
23 # and this assignment fails with:
24 # Incompatible types in assignment (expression has type
25 # "tuple[ResponseSpec, ResponseSpec]", base class "MyBaseController"
26 # defined the type as "tuple[ResponseSpec]")
27 responses = (
28 *MyBaseController.responses,
29 ResponseSpec(ErrorModel, status_code=HTTPStatus.CONFLICT),
30 )
Generic parsing and response models#
Next, let’s define a reusable controller that will have:
customizable serializer
customizable request model
customizable response body
Raw endpoints support#
The process will look exactly the same:
1from abc import abstractmethod
2from typing import Generic, TypeVar
3
4from dmr import Body, Controller
5from dmr.serializer import BaseSerializer
6
7_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
8_RequestModelT = TypeVar('_RequestModelT')
9_ResponseBodyT = TypeVar('_ResponseBodyT')
10
11
12class ReusableController(
13 Controller[_SerializerT],
14 Generic[_SerializerT, _RequestModelT, _ResponseBodyT],
15):
16 def post(self, parsed_body: Body[_RequestModelT]) -> _ResponseBodyT:
17 return self.convert(parsed_body)
18
19 @abstractmethod
20 def convert(self, parsed_body: _RequestModelT) -> _ResponseBodyT:
21 raise NotImplementedError
Here we use 3 type variables. One of each of the parts we want to customize.
Important part here is that we defined our own abstract convert method
to convert unknown request model into an unknown response body.
We would need to implement this method in all of our concrete controllers.
1from typing_extensions import TypedDict, override
2
3from dmr.plugins.msgspec import MsgspecSerializer
4from examples.reusable_code.reusable_parsing import ReusableController
5
6
7class _RequestModel(TypedDict):
8 username: str
9
10
11class _ResponseBody(TypedDict):
12 message: str
13
14
15class MsgspecController(
16 ReusableController[MsgspecSerializer, _RequestModel, _ResponseBody],
17):
18 @override
19 def convert(self, parsed_body: _RequestModel) -> _ResponseBody:
20 return {'message': f'Hello, {parsed_body["username"]}'}
21
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
{"message":"Hello, sobolevn"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"username": {
"type": "string"
}
},
"required": [
"username"
],
"title": "_RequestModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"title": "_ResponseBody",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/msgspeccontroller/": {
"post": {
"operationId": "postMsgspecControllerApiMsgspeccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1from typing_extensions import TypedDict, override
2
3from dmr.plugins.pydantic import PydanticFastSerializer
4from examples.reusable_code.reusable_parsing import ReusableController
5
6
7class _RequestModel(TypedDict):
8 first_name: str
9 last_name: str
10
11
12class _ResponseBody(TypedDict):
13 full_name: str
14
15
16class PydanticController(
17 ReusableController[PydanticFastSerializer, _RequestModel, _ResponseBody],
18):
19 @override
20 def convert(self, parsed_body: _RequestModel) -> _ResponseBody:
21 return {
22 'full_name': (
23 f'{parsed_body["first_name"]} {parsed_body["last_name"]}'
24 ),
25 }
26
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
{"full_name":"Nikita Sobolev"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"first_name": {
"title": "First Name",
"type": "string"
},
"last_name": {
"title": "Last Name",
"type": "string"
}
},
"required": [
"first_name",
"last_name"
],
"title": "_RequestModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"full_name": {
"title": "Full Name",
"type": "string"
}
},
"required": [
"full_name"
],
"title": "_ResponseBody",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/pydanticcontroller/": {
"post": {
"operationId": "postPydanticControllerApiPydanticcontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Note that msgspec and pydantic controllers in this
case have completely different request and response bodies
and completely different OpenAPI schemas.
We can completely customize each controller and all parsing components and return type validation.
Important
All schema generation and validation rules work the same way for concrete controllers.
We infer the passed values during import time and use real types.
Real endpoints support#
Added in version 0.14.0.
The same would work with endpoints defined
with validate decorator.
The logic is the same, but syntax is a bit different.
Tip
By default mypy and other type-checkers won’t allow to write
ResponseSpec(_TypeT, status_code=OK), because type vars can’t be used
in such places according
to the typing spec.
So, we provide dmr.types.safe_typevar() helper
to get rid of the type-checking errors.
Here’s how we can do the same example, but with @validate.
The reusable part:
1from abc import abstractmethod
2from http import HTTPStatus
3from typing import Generic, TypeVar
4
5from django.http import HttpResponse
6
7from dmr import Body, Controller, ResponseSpec, validate
8from dmr.serializer import BaseSerializer
9from dmr.types import safe_typevar
10
11_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
12_RequestModelT = TypeVar('_RequestModelT')
13_ResponseBodyT = TypeVar('_ResponseBodyT')
14
15
16class ReusableController(
17 Controller[_SerializerT],
18 Generic[_SerializerT, _RequestModelT, _ResponseBodyT],
19):
20 @validate(
21 ResponseSpec(
22 safe_typevar('_ResponseBodyT'),
23 status_code=HTTPStatus.CREATED,
24 ),
25 )
26 def post(self, parsed_body: Body[_RequestModelT]) -> HttpResponse:
27 return self.to_response(self.convert(parsed_body))
28
29 @abstractmethod
30 def convert(self, parsed_body: _RequestModelT) -> _ResponseBodyT:
31 raise NotImplementedError
And then - implementations:
1import msgspec
2from typing_extensions import override
3
4from dmr.plugins.msgspec import MsgspecSerializer
5from examples.reusable_code.validate_reusable import ReusableController
6
7
8class _RequestModel(msgspec.Struct):
9 username: str
10
11
12class _ResponseBody(msgspec.Struct):
13 message: str
14
15
16class MsgspecController(
17 ReusableController[MsgspecSerializer, _RequestModel, _ResponseBody],
18):
19 @override
20 def convert(self, parsed_body: _RequestModel) -> _ResponseBody:
21 return _ResponseBody(message=f'Hello, {parsed_body.username}')
22
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
{"message":"Hello, sobolevn"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"type": "array"
},
"msg": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"username": {
"type": "string"
}
},
"required": [
"username"
],
"title": "_RequestModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"title": "_ResponseBody",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/msgspeccontroller/": {
"post": {
"operationId": "postMsgspecControllerApiMsgspeccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1import pydantic
2from typing_extensions import override
3
4from dmr.plugins.pydantic import PydanticFastSerializer
5from examples.reusable_code.validate_reusable import ReusableController
6
7
8class _RequestModel(pydantic.BaseModel):
9 first_name: str
10 last_name: str
11
12
13class _ResponseBody(pydantic.BaseModel):
14 full_name: str
15
16
17class PydanticController(
18 ReusableController[PydanticFastSerializer, _RequestModel, _ResponseBody],
19):
20 @override
21 def convert(self, parsed_body: _RequestModel) -> _ResponseBody:
22 return _ResponseBody(
23 full_name=(f'{parsed_body.first_name} {parsed_body.last_name}'),
24 )
25
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
{"full_name":"Nikita Sobolev"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"first_name": {
"title": "First Name",
"type": "string"
},
"last_name": {
"title": "Last Name",
"type": "string"
}
},
"required": [
"first_name",
"last_name"
],
"title": "_RequestModel",
"type": "object"
},
"_ResponseBody": {
"properties": {
"full_name": {
"title": "Full Name",
"type": "string"
}
},
"required": [
"full_name"
],
"title": "_ResponseBody",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/pydanticcontroller/": {
"post": {
"operationId": "postPydanticControllerApiPydanticcontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseBody"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
This way offers you more control over the response headers, cookies, etc. Choose the one that fits best of the job.
Type variable defaults#
Added in version 0.16.0.
Type variables can have defaults, as described in PEP 696. We use them when a subclass does not provide some of the type args.
There are three ways to define them:
typing_extensions.TypeVar('_ModelT', default=MyModel)on any versiontyping.TypeVarwith the samedefault=argument on 3.13 and abovethe native
class Reusable[_ModelT = MyModel]syntax on 3.13 and above
1from typing import Generic
2
3from typing_extensions import TypedDict, TypeVar
4
5from dmr import Body, Controller
6from dmr.serializer import BaseSerializer
7
8
9class DefaultRequestModel(TypedDict):
10 first_name: str
11 last_name: str
12
13
14_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
15_RequestModelT = TypeVar('_RequestModelT', default=DefaultRequestModel)
16
17
18class ReusableController(
19 Controller[_SerializerT],
20 Generic[_SerializerT, _RequestModelT],
21):
22 def post(self, parsed_body: Body[_RequestModelT]) -> _RequestModelT:
23 return parsed_body
The request model is now optional for the subclasses:
1from dmr.plugins.pydantic import PydanticFastSerializer
2from examples.reusable_code.reusable_defaults import ReusableController
3
4
5class PydanticController(ReusableController[PydanticFastSerializer]):
6 """The request model is not given, so it is `DefaultRequestModel`."""
7
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
{"first_name":"Nikita","last_name":"Sobolev"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"DefaultRequestModel": {
"properties": {
"first_name": {
"title": "First Name",
"type": "string"
},
"last_name": {
"title": "Last Name",
"type": "string"
}
},
"required": [
"first_name",
"last_name"
],
"title": "DefaultRequestModel",
"type": "object"
},
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/pydanticcontroller/": {
"post": {
"operationId": "postPydanticControllerApiPydanticcontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DefaultRequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DefaultRequestModel"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"summary": "The request model is not given, so it is `DefaultRequestModel`."
}
}
}
1from typing_extensions import TypedDict
2
3from dmr.plugins.pydantic import PydanticFastSerializer
4from examples.reusable_code.reusable_defaults import ReusableController
5
6
7class _RequestModel(TypedDict):
8 email: str
9
10
11class PydanticController(
12 ReusableController[PydanticFastSerializer, _RequestModel],
13):
14 """
15 Defaults are only used when type args are missing.
16
17 ``DefaultRequestModel`` is fully replaced here,
18 so its payload does not validate anymore.
19 """
20
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"email": "example@example.com"}' -H 'Content-Type: application/json'
{"email":"example@example.com"}
$ curl http://127.0.0.1:8000/api/example/ -D - -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
date: Wed, 30 Sep 2026 23:19:46 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 88
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Field required","loc":["parsed_body","email"],"type":"value_error"}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"email": {
"title": "Email",
"type": "string"
}
},
"required": [
"email"
],
"title": "_RequestModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/pydanticcontroller/": {
"description": "``DefaultRequestModel`` is fully replaced here,\nso its payload does not validate anymore.",
"post": {
"operationId": "postPydanticControllerApiPydanticcontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"summary": "Defaults are only used when type args are missing."
}
}
}
Defaults can also point to other type variables:
_ResponseBodyT = TypeVar('_ResponseBodyT', default=_RequestModelT)
means “the response body is the request model, unless told otherwise”.
Serializers can have defaults as well. Then a subclass that passes no type args at all is a concrete controller, because it has an exact serializer:
1from typing import Generic
2
3from typing_extensions import TypedDict, TypeVar
4
5from dmr import Body, Controller
6from dmr.plugins.pydantic import PydanticFastSerializer
7from dmr.serializer import BaseSerializer
8
9
10class _RequestModel(TypedDict):
11 first_name: str
12 last_name: str
13
14
15_SerializerT = TypeVar(
16 '_SerializerT',
17 bound=BaseSerializer,
18 default=PydanticFastSerializer,
19)
20_RequestModelT = TypeVar('_RequestModelT', default=_RequestModel)
21
22
23class ReusableController(
24 Controller[_SerializerT],
25 Generic[_SerializerT, _RequestModelT],
26):
27 """Still abstract: its own type variables are not exact types."""
28
29 def post(self, parsed_body: Body[_RequestModelT]) -> _RequestModelT:
30 return parsed_body
31
32
33class PydanticController(ReusableController):
34 """Concrete: both type variables fall back to their defaults."""
35
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
{"first_name":"Nikita","last_name":"Sobolev"}
Important
A default only applies to the subclasses, never to the reusable
controller that declares it. ReusableController above still has
is_abstract set to True and cannot be routed,
even though every one of its type variables has a default.
This is the same rule as everywhere else: we only build endpoints for concrete controllers, and a type variable is not an exact type.
Note
A type variable without a default is still required. Controllers that don’t have an exact serializer stay abstract, as always.
Write the subclass when you have anything else to say: a setting to change, a hook to redefine, or a name to route several times.
Lazy reusable endpoints#
In some cases you need to do even more than request / response payload modifications. Sometimes, you need to change the status code, header and cookie specs, maybe even auth or throttling definitions.
Important
We only create endpoints when concrete controller is built. We never create endpoins definitions for reusable controllers. So, all endpoints are always created in the correct - final - context.
To use the full customization, we provide:
dmr.endpoint.ModifyEndpoint.lazy()method to work withmodify. It accepts a function or aclassmethodto lazily provide a spec in the futuredmr.endpoint.ValidateEndpoint.lazy()method to work withvalidate. It accepts a function or aclassmethodto lazily provide a spec in the future
Here’s how it works:
1from http import HTTPStatus
2from typing import ClassVar, TypeVar
3
4from dmr import Controller, modify
5from dmr.endpoint import ModifyAnyCallable
6from dmr.serializer import BaseSerializer
7
8_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
9
10
11class LoginController(Controller[_SerializerT]):
12 status_code: ClassVar[HTTPStatus] = HTTPStatus.OK
13
14 @classmethod
15 def lazy_spec(cls) -> ModifyAnyCallable:
16 return modify(status_code=cls.status_code)
17
18 @modify.lazy(lazy_spec)
19 def get(self) -> str:
20 return 'login'
1from http import HTTPStatus
2from typing import ClassVar, TypeVar
3
4from django.http import HttpResponse
5
6from dmr import Controller, ResponseSpec, validate
7from dmr.endpoint import ValidateAnyCallable
8from dmr.serializer import BaseSerializer
9
10_SerializerT = TypeVar('_SerializerT', bound=BaseSerializer)
11
12
13class LoginController(Controller[_SerializerT]):
14 status_code: ClassVar[HTTPStatus] = HTTPStatus.IM_USED
15
16 @classmethod
17 def lazy_spec(cls) -> ValidateAnyCallable:
18 return validate(ResponseSpec(str, status_code=cls.status_code))
19
20 @validate.lazy(lazy_spec)
21 def get(self) -> HttpResponse:
22 return self.to_response('login', status_code=self.status_code)
What happens here?
We define a reusable controller with lazy endpoint specification
We define
lazy_specclassmethod that will provide the actual decorator during the child - final - controller build timeWe use class-level API to define constants that people can modify in their child - final - controllers if needed. But, the default implementation would work the way we described it
Notice that we use special types to define
the return type from the lazy_spec classmethod.
Here are all of them, choose the one for your task:
Type name |
Original decorator |
What it does |
|---|---|---|
Creates a decorator for endpoints without sync / async specifics |
||
Creates a decorator for sync endpoints |
||
Creates a decorator for async endpoints |
||
Creates a decorator for endpoints without sync / async specifics |
||
Creates a decorator for sync endpoints |
||
Creates a decorator for async endpoints |
Basically, there are several major rules:
Prefer defining decorators that do not care about sync / async code, use
ModifyAnyCallableandValidateAnyCallableby defaultIf you need sync / async details like
error_handler,auth, orthrottling- then use exact type for sync / async decorator of your choiceDo not mix specs for
@validateand@modify, it will be a type error
Customizing definitions#
Now, let’s use the reusable controller we defined above, but we would customize the response status code, just as an example of power that we have:
1from http import HTTPStatus
2from typing import ClassVar
3
4from dmr.plugins.pydantic import PydanticFastSerializer
5from examples.reusable_code.lazy_modify import LoginController
6
7
8class OurLoginController(LoginController[PydanticFastSerializer]):
9 status_code: ClassVar[HTTPStatus] = HTTPStatus.IM_USED
10
Run result
$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 226 IM Used
date: Wed, 30 Sep 2026 23:19:47 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 7
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
"login"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/ourlogincontroller/": {
"get": {
"operationId": "getOurLoginControllerApiOurlogincontroller",
"responses": {
"226": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "IM Used"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1from http import HTTPStatus
2from typing import ClassVar
3
4from dmr.plugins.pydantic import PydanticFastSerializer
5from examples.reusable_code.lazy_validate import LoginController
6
7
8class OurLoginController(LoginController[PydanticFastSerializer]):
9 status_code: ClassVar[HTTPStatus] = HTTPStatus.IM_USED
10
Run result
$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 226 IM Used
date: Wed, 30 Sep 2026 23:19:48 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 7
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
"login"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/ourlogincontroller/": {
"get": {
"operationId": "getOurLoginControllerApiOurlogincontroller",
"responses": {
"226": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "IM Used"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Notice that the response code would be changed in both the spec and runtime. And, of course, you can combine this approach with generic serializer, or request and response payloads. Giving the full control over code reuse.
Overriding definitions#
To finish this example off, we would completely override the spec in a child controller. Sometimes users might want to do that, for example: to provide auth or custom OpenAPI spec. But, user is free to modify any parts of the spec, if needed.
1from http import HTTPStatus
2from typing import ClassVar
3
4from typing_extensions import override
5
6from dmr import modify
7from dmr.endpoint import ModifyAnyCallable
8from dmr.plugins.pydantic import PydanticFastSerializer
9from examples.reusable_code.lazy_modify import LoginController
10
11
12class OurLoginController(LoginController[PydanticFastSerializer]):
13 status_code: ClassVar[HTTPStatus] = HTTPStatus.IM_USED
14
15 @classmethod
16 @override
17 def lazy_spec(cls) -> ModifyAnyCallable:
18 return modify(status_code=cls.status_code, tags=['custom_openapi_meta'])
19
Run result
$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 226 IM Used
date: Wed, 30 Sep 2026 23:19:48 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 7
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
"login"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/ourlogincontroller/": {
"get": {
"operationId": "getOurLoginControllerApiOurlogincontroller",
"responses": {
"226": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "IM Used"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
},
"tags": [
"custom_openapi_meta"
]
}
}
}
}
1from http import HTTPStatus
2from typing import ClassVar
3
4from typing_extensions import override
5
6from dmr import ResponseSpec, validate
7from dmr.endpoint import ValidateAnyCallable
8from dmr.plugins.pydantic import PydanticFastSerializer
9from examples.reusable_code.lazy_validate import LoginController
10
11
12class OurLoginController(LoginController[PydanticFastSerializer]):
13 status_code: ClassVar[HTTPStatus] = HTTPStatus.IM_USED
14
15 @classmethod
16 @override
17 def lazy_spec(cls) -> ValidateAnyCallable:
18 return validate(
19 ResponseSpec(str, status_code=cls.status_code),
20 tags=['custom_openapi_meta'],
21 )
22
Run result
$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 226 IM Used
date: Wed, 30 Sep 2026 23:19:49 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 7
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
"login"
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/ourlogincontroller/": {
"get": {
"operationId": "getOurLoginControllerApiOurlogincontroller",
"responses": {
"226": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "IM Used"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
},
"tags": [
"custom_openapi_meta"
]
}
}
}
}
Note
Notice that lazy_spec classmethod is resolve from the final controller,
not the one that was used during the decoration time.
@classmethod is preferable over lambda functions,
because they provide easier override API and they are fully typed.
The controller argument of a lambda is typed as type[Any],
so attribute access on it is not checked by type checkers.
Where is it actually helpful in practice?#
We use this feature a lot in the pre-defined views we provide with the framework.
For example, we use this in JWT Auth obtain views:
ObtainTokensSyncControllerfor sync controllersObtainTokensAsyncControllerfor async controllers
Usage example:
1import datetime as dt
2
3from typing_extensions import override
4
5from dmr.plugins.pydantic import PydanticFastSerializer
6from dmr.security.jwt.views import (
7 ObtainTokensPayload,
8 ObtainTokensResponse,
9 ObtainTokensSyncController,
10)
11
12
13# You can also use `ObtainTokensAsyncController` if needed:
14class ObtainAccessAndRefreshSyncController(
15 ObtainTokensSyncController[
16 PydanticFastSerializer,
17 ObtainTokensPayload,
18 ObtainTokensResponse,
19 ],
20):
21 @override
22 def convert_auth_payload(
23 self,
24 payload: ObtainTokensPayload,
25 ) -> ObtainTokensPayload:
26 return payload
27
28 @override
29 def make_api_response(self) -> ObtainTokensResponse:
30 now = dt.datetime.now(dt.UTC)
31 return {
32 'access_token': self.create_jwt_token(
33 expiration=now + self.jwt_expiration,
34 token_type='access',
35 ),
36 'refresh_token': self.create_jwt_token(
37 expiration=now + self.jwt_refresh_expiration,
38 token_type='refresh',
39 ),
40 }
41
Run result
$ curl http://127.0.0.1:8000/api/auth/ -X POST -d '{"username": "test_user", "password": "password"}' -H 'Content-Type: application/json'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkwODk2NzkwLCJpYXQiOjE3OTA4MTAzOTAsImp0aSI6IjQ2ZWJhODNkZjNjYjRhYWI5MmFjMWNkYTZlYjkxYmUwIiwiZXh0cmFzIjp7InR5cGUiOiJhY2Nlc3MifX0.Tq-ZR5dCJLEy-EAtAOEUgfevtLe4UmmFLOvXWJmh4nc","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkxNjc0MzkwLCJpYXQiOjE3OTA4MTAzOTAsImp0aSI6Ijk4OTE0ZmU1NjM5NjQxYTFhZDIxYWRlMGFlMWUxNTc4IiwiZXh0cmFzIjp7InR5cGUiOiJyZWZyZXNoIn19.enPuJ9zdXptEOBgsGRIsl9_12vTsVqNABx2KNTPOoyg"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"ObtainTokensPayload": {
"description": "Payload for default version of a jwt request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
"properties": {
"password": {
"title": "Password",
"type": "string"
},
"username": {
"title": "Username",
"type": "string"
}
},
"required": [
"username",
"password"
],
"title": "ObtainTokensPayload",
"type": "object"
},
"ObtainTokensResponse": {
"description": "Default response type for refresh token endpoint.",
"properties": {
"access_token": {
"title": "Access Token",
"type": "string"
},
"refresh_token": {
"title": "Refresh Token",
"type": "string"
}
},
"required": [
"access_token",
"refresh_token"
],
"title": "ObtainTokensResponse",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/obtainaccessandrefreshsynccontroller/": {
"post": {
"operationId": "postObtainAccessAndRefreshSyncControllerApiObtainaccessandrefreshsynccontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ObtainTokensPayload"
}
}
},
"description": "Payload for default version of a jwt request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ObtainTokensResponse"
}
}
},
"description": "OK",
"headers": {
"Cache-Control": {
"description": "Credentials must not be stored in any cache.",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Unauthorized"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
},
"summary": "By default tokens are acquired on post."
}
}
}
}
Why is it useful?
We can work with any serializer
We can change our request payload to be whatever we need, it would be correctly rendered in the final OpenAPI schema
We can change the response schema, which would also be correctly rendered in the OpenAPI
This feature allows us to have type-safe and OpenAPI-first approach to code reusability, great DX, and Python-native abstractions.
Users / plugin developers can do the same to provide universal customizable controllers.