Plugins¶
To be able to support multiple serializer models
like pydantic and msgspec, we have a concept of a plugin.
There are several bundled ones, but you can write your own as well. To do that see our advanced Serialization guide.
As a user you are only interested in choosing the right plugin for the controller definition.
from dmr.plugins.msgspec import MsgspecSerializer
Tip
If you only use json parsers and renderers,
it would be faster to use
PydanticFastSerializer instead.
from dmr.plugins.pydantic import PydanticSerializer
Customizing serializers¶
There are several things why you can possibly want to customize an existing serializer.
Support more data types¶
By default,
serialize_hook()
and
deserialize_hook()
support not that many types.
You can customize the serializer to know how to serializer / deserialize more types by extending it and customizing the method you need.
Customizing the serializer context¶
We use dmr.endpoint.SerializerContext type
to deserialize all components from a single model, so it would be much faster
than parsing each component separately.
This class can be customized for several reasons.
Change the default strictness¶
Tools like pydantic offer several useful type conversions in non-strict mode.
For example, '1' can be parsed as 1 if strict mode is not enabled.
It is kinda useful for request bodies, where you don’t control the clients.
Here’s how we determine the default strictness for pydantic models:
If
strict_validationis notNone, we return the serializer-level strictnessThen
pydanticlooks atstrictattribute inConfigDictThen
pydanticlooks atstrictattribute for individualField()items
We recommend to change the strictness on a per-model basis, but if you want to,
you can subclass the SerializerContext to be strict / non-strict
and use it for all controllers.
Endpoint optimizers¶
Before actually serving any requests, during import-time, we try to optimize the future validation.
For example, pydantic.TypeAdapter takes time to be created.
Why doing it on the first request, when we can do that during the import time?
Each serializer must provide a type, which must be a subclass
of BaseEndpointOptimizer
to optimize / pre-compile / create / cache things that it can.
Writing a custom plugin¶
Our API is flexible enough to potentially support any custom third-party serializers of your choice, like:
Follow the API of PydanticSerializer
and MsgspecSerializer.
You would need to:
Provide a way to serializer and deserialize your models
Provide serializer error converter by overriding
serialize_validation_error()methodProvide a way to get the OpenAPI / JsonSchema schema from your models, see
dmr.serializer.BaseSchemaGenerator. Example implementations:PydanticSchemaGeneratorandMsgspecSchemaGenerator
Pydantic plugin¶
PydanticFastSerializer¶
pydantic plugin contains one extra serializer optimized for json usage.
Our regular API requires parsers and renderers
to format the final response,
so you can negotiate the request and response formats.
However, for cases when you only have json requests
and responses (which is quite common), use
PydanticFastSerializer.
Warning
It will ignore all parsers and serializers and use the pydantic
own way to serialize and deserialize objects to json bytestring.
It will work from 3 up 10 times faster depending on the data then the common serializer.
1import pydantic
2
3from dmr import Body, Controller
4from dmr.plugins.pydantic import PydanticFastSerializer
5
6
7class _User(pydantic.BaseModel):
8 username: str
9 age: int
10
11
12class UserController(Controller[PydanticFastSerializer]):
13 def put(self, parsed_body: Body[_User]) -> _User:
14 return parsed_body
15
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT -d '{"username": "sobolevn", "age": 27}' -H 'Content-Type: application/json'
{"username":"sobolevn","age":27}
$ curl http://127.0.0.1:8000/api/users/ -D - -X PUT -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
date: Tue, 26 May 2026 19:08:45 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 86
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Field required","loc":["parsed_body","age"],"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"
},
"_User": {
"properties": {
"age": {
"title": "Age",
"type": "integer"
},
"username": {
"title": "Username",
"type": "string"
}
},
"required": [
"username",
"age"
],
"title": "_User",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"put": {
"deprecated": false,
"operationId": "putUsercontrollerApiUsercontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_User"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_User"
}
}
},
"description": "OK"
},
"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"
}
}
}
}
}
}
No API changes are required to use it if you don’t use other request / response formats.
Serialization / deserialization flags¶
We have to special attributes to change how pydantic serializes data:
to_json_kwargsfor serialization purposesto_model_kwargsfor deserialization purposes
By default these flags only pass {'by_alias': True}
to support field aliases, when they are defined.
For example, when working with pydantic.types.Json,
one can set round_trip to True
(which is not passed by default,
because it disables computed fields):
1from typing import ClassVar
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.plugins.pydantic import PydanticFastSerializer
7from dmr.plugins.pydantic.serializer import ToJsonKwargs
8
9
10class _User(pydantic.BaseModel):
11 username: str
12 settings: pydantic.Json[dict[str, str]]
13
14
15class _RoundTripPydantic(PydanticFastSerializer):
16 to_json_kwargs: ClassVar[ToJsonKwargs] = {
17 **PydanticFastSerializer.to_json_kwargs,
18 'round_trip': True,
19 }
20
21
22class UserController(Controller[_RoundTripPydantic]):
23 def post(self, parsed_body: Body[_User]) -> _User:
24 return parsed_body
25
Run result
$ curl http://127.0.0.1:8000/api/users/ -X POST -d '{"username": "sobolevn", "settings": "{\"status\": \"active\"}"}' -H 'Content-Type: application/json'
{"username":"sobolevn","settings":"{\"status\":\"active\"}"}
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"
},
"_User": {
"properties": {
"settings": {
"contentMediaType": "application/json",
"contentSchema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"title": "Settings",
"type": "string"
},
"username": {
"title": "Username",
"type": "string"
}
},
"required": [
"username",
"settings"
],
"title": "_User",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_User"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_User"
}
}
},
"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"
}
}
}
}
}
}
See also
Msgspec plugin¶
attrs support¶
We support attrs.define() via msgspec compatibility layer.
It has its own limitations.
See msgspec docs.
Native support of attrs can be implemented in the future
with its own serializer.