Content negotiation¶
django-modern-rest supports content negotiation.
We have two abstractions to do that:
Parsers: instances of subtypes of
Parsertype that parses request body based on Content-Type header into python primitivesRenderers: instances of subtypes of
Renderertype that renders python primitives into a requested format based on the Accept header
By default json parser and renderer are configured
to use msgspec if it is installed (recommended).
We fallback to pure-python implementation if msgspec is not installed.
Supported content types¶
We ship several pre-defined parsers and renderers.
Parsers:
application/jsonwithMsgspecJsonParserandJsonParserapplication/msgpackwithMsgpackParsermultipart/form-datawithMultiPartParserapplication/x-www-form-urlencodedwithFormUrlEncodedParser
Renderers:
application/jsonwithMsgspecJsonRendererandJsonRendererapplication/msgpackwithMsgpackRenderer*/*withFileRenderer
You can write your own!
How parser and renderer are selected¶
We select a Parser instance
if there’s a Body
or FileMetadata components to parse.
Otherwise, for performance reasons, no parser is selected at all.
Nothing to parse - no parser is selected.
Here’s how we select a parser, when it is needed:
We look at the
Content-TypeheaderIf it is not provided, we take the default parser, which is the first specified parser for the endpoint, aka the most specific one
If there’s a
Content-Typeheader, we try to exactly match known parsers based on theircontent_typeattribute. This is a positive path optimizationIf there’s no direct match, we now include parsers that have
*pattern in supported content types. We match them in order based on'specificity', 'quality', the first match winsIf no parser fits the request’s content type, we raise
RequestSerializationError
We select Renderer instance
for all responses (including error responses), before performing any logic.
If the selection fails, we don’t even try to run the endpoint.
Here’s how we select a renderer:
We look at
AcceptheaderIf it is not provided, we take the default renderer, which is the first specified renderer for the endpoint, aka the most specific one
If there’s an
Acceptheader, we use all renderers specified for this endpoint to match the best accepted type, based onquality, specificity, the first match winsIf no renderer fits for the accepted content types, we raise
ResponseSchemaError
Note
When constructing responses manually, like:
>>> from django.http import HttpResponse
>>> response = HttpResponse(b'[]')
The renderer is selected as usual, but no actual rendering is done. However, all other validation works as expected. Which means that even though renderer is not actually used, its metadata is still required to validate the response content type.
But, when using to_response()
method, renderer will be executed.
So, it is a preferred method for regular responses.
Important
Settings always must have one parser
and one renderer defined at all times,
because utils like dmr.response.build_response()
fallbacks to settings-defined renderers in some error cases.
Alternative JSON backends¶
By default, we use msgspec if it installed.
When it is not, we fallback to JsonParser
and JsonRenderer types, which use native pure Python
json with limited features support and very low performance.
For users, who does not want to use msgspec, but prefer
orjson for some reason,
we provide the following API:
1import orjson # type: ignore[import-not-found, unused-ignore]
2import pydantic
3
4from dmr import Body, Controller
5from dmr.parsers import JsonParser
6from dmr.plugins.pydantic import PydanticSerializer
7from dmr.renderers import JsonRenderer
8
9
10class _UserInputData(pydantic.BaseModel):
11 email: pydantic.EmailStr
12
13
14class UserController(Controller[PydanticSerializer]):
15 parsers = (JsonParser(json_module=orjson),)
16 renderers = (JsonRenderer(json_module=orjson),)
17
18 def post(
19 self,
20 parsed_body: Body[_UserInputData],
21 ) -> _UserInputData:
22 return parsed_body
23
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@example.com"}' -H 'Content-Type: application/json'
{"email":"user@example.com"}
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"
},
"_UserInputData": {
"properties": {
"email": {
"format": "email",
"title": "Email",
"type": "string"
}
},
"required": [
"email"
],
"title": "_UserInputData",
"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/_UserInputData"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_UserInputData"
}
}
},
"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"
}
}
}
}
}
}
Any module that exposes API that fits JsonModule
protocol is supported. If some API does not fit exactly, you can create
a small wrapper that would fit, like NativeJson.
orjson is the recommended alternative because
it is really fast, returns bytes directly,
avoiding an extra encode step, and is significantly
faster than the standard library json.
Customizing negotiation process¶
Note
If you only use json API - there’s no need to change anything.
However, if you want to support other formats like xml or custom ones,
you can write and configure your own parsers and renderers.
Parsers and renderers might be defined on different levels. Here are all the possible ways starting with the most specific one, going back to the less specific:
1import uuid
2from typing import Generic, TypeVar
3
4import pydantic
5
6from dmr import Body, Controller, modify
7from dmr.plugins.pydantic import PydanticSerializer
8from examples.negotiation.negotiation import XmlParser, XmlRenderer
9
10
11class _UserProfile(pydantic.BaseModel):
12 age: int
13
14
15class _UserInputData(pydantic.BaseModel):
16 email: str
17 profile: _UserProfile
18
19
20class _UserOutputData(_UserInputData):
21 uid: uuid.UUID
22
23
24_ModelT = TypeVar('_ModelT')
25
26
27class _UserDocument(pydantic.BaseModel, Generic[_ModelT]):
28 user: _ModelT
29
30
31class UserController(Controller[PydanticSerializer]):
32 @modify(parsers=[XmlParser()], renderers=[XmlRenderer()])
33 def post(
34 self,
35 parsed_body: Body[_UserDocument[_UserInputData]],
36 ) -> _UserDocument[_UserOutputData]:
37 return _UserDocument(
38 user=_UserOutputData(
39 uid=uuid.uuid4(),
40 email=parsed_body.user.email,
41 profile=parsed_body.user.profile,
42 ),
43 )
44
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '<request><user><email>user@example.com</email><profile><age>28</age></profile></user></request>' -H 'Content-Type: application/xml' -H 'Accept: application/xml'
<?xml version="1.0" encoding="utf-8"?>
<_UserDocument><user><email>user@example.com</email><profile><age>28</age></profile><uid>9178b669-8ea6-431a-8fda-97a22c41cea9</uid></user></_UserDocument>
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"
},
"_UserDocument[_UserInputData]": {
"properties": {
"user": {
"$ref": "#/components/schemas/_UserInputData"
}
},
"required": [
"user"
],
"title": "_UserDocument[_UserInputData]",
"type": "object"
},
"_UserDocument[_UserOutputData]": {
"properties": {
"user": {
"$ref": "#/components/schemas/_UserOutputData"
}
},
"required": [
"user"
],
"title": "_UserDocument[_UserOutputData]",
"type": "object"
},
"_UserInputData": {
"properties": {
"email": {
"title": "Email",
"type": "string"
},
"profile": {
"$ref": "#/components/schemas/_UserProfile"
}
},
"required": [
"email",
"profile"
],
"title": "_UserInputData",
"type": "object"
},
"_UserOutputData": {
"properties": {
"email": {
"title": "Email",
"type": "string"
},
"profile": {
"$ref": "#/components/schemas/_UserProfile"
},
"uid": {
"format": "uuid",
"title": "Uid",
"type": "string"
}
},
"required": [
"email",
"profile",
"uid"
],
"title": "_UserOutputData",
"type": "object"
},
"_UserProfile": {
"properties": {
"age": {
"title": "Age",
"type": "integer"
}
},
"required": [
"age"
],
"title": "_UserProfile",
"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/xml": {
"schema": {
"$ref": "#/components/schemas/_UserDocument[_UserInputData]"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_UserDocument[_UserOutputData]"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1import uuid
2from typing import Generic, TypeVar
3
4import pydantic
5
6from dmr import Body, Controller
7from dmr.plugins.msgspec import MsgspecJsonParser, MsgspecJsonRenderer
8from dmr.plugins.pydantic import PydanticSerializer
9from examples.negotiation.negotiation import XmlParser, XmlRenderer
10
11
12class _UserProfile(pydantic.BaseModel):
13 age: int
14
15
16class _UserInputData(pydantic.BaseModel):
17 email: str
18 profile: _UserProfile
19
20
21class _UserOutputData(_UserInputData):
22 uid: uuid.UUID
23
24
25_ModelT = TypeVar('_ModelT')
26
27
28class _UserDocument(pydantic.BaseModel, Generic[_ModelT]):
29 user: _ModelT
30
31
32class UserController(Controller[PydanticSerializer]):
33 parsers = (MsgspecJsonParser(), XmlParser())
34 renderers = (MsgspecJsonRenderer(), XmlRenderer())
35
36 def post(
37 self,
38 parsed_body: Body[_UserDocument[_UserInputData]],
39 ) -> _UserDocument[_UserOutputData]:
40 return _UserDocument(
41 user=_UserOutputData(
42 uid=uuid.uuid4(),
43 email=parsed_body.user.email,
44 profile=parsed_body.user.profile,
45 ),
46 )
47
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '<request><user><email>user@example.com</email><profile><age>28</age></profile></user></request>' -H 'Content-Type: application/xml' -H 'Accept: application/json'
{"user":{"email":"user@example.com","profile":{"age":28},"uid":"75066f5d-4a3a-43df-9d47-51357408c527"}}
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"
},
"_UserDocument[_UserInputData]": {
"properties": {
"user": {
"$ref": "#/components/schemas/_UserInputData"
}
},
"required": [
"user"
],
"title": "_UserDocument[_UserInputData]",
"type": "object"
},
"_UserDocument[_UserOutputData]": {
"properties": {
"user": {
"$ref": "#/components/schemas/_UserOutputData"
}
},
"required": [
"user"
],
"title": "_UserDocument[_UserOutputData]",
"type": "object"
},
"_UserInputData": {
"properties": {
"email": {
"title": "Email",
"type": "string"
},
"profile": {
"$ref": "#/components/schemas/_UserProfile"
}
},
"required": [
"email",
"profile"
],
"title": "_UserInputData",
"type": "object"
},
"_UserOutputData": {
"properties": {
"email": {
"title": "Email",
"type": "string"
},
"profile": {
"$ref": "#/components/schemas/_UserProfile"
},
"uid": {
"format": "uuid",
"title": "Uid",
"type": "string"
}
},
"required": [
"email",
"profile",
"uid"
],
"title": "_UserOutputData",
"type": "object"
},
"_UserProfile": {
"properties": {
"age": {
"title": "Age",
"type": "integer"
}
},
"required": [
"age"
],
"title": "_UserProfile",
"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/_UserDocument[_UserInputData]"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_UserDocument[_UserInputData]"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_UserDocument[_UserOutputData]"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_UserDocument[_UserOutputData]"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
First parsers / renderers definition found, starting from the top, will win and be used for the endpoint.
You can also modify
dmr.endpoint.Endpoint.request_negotiator_cls
and dmr.endpoint.Endpoint.response_negotiator_cls
to completely change the negotiation logic to fit your needs.
This is possible on per-controller level.
Writing custom parsers and renderers¶
And here’s how our test xml parser and renderer are defined:
1from collections.abc import Callable
2from typing import Any, final
3from xml.parsers import expat
4
5import xmltodict_rs as xmltodict
6from django.http import HttpRequest
7from typing_extensions import override
8
9from dmr.exceptions import DataRenderingError, RequestSerializationError
10from dmr.negotiation import ContentType
11from dmr.parsers import DeserializeFunc, Parser, Raw
12from dmr.renderers import Renderer
13
14
15@final
16class XmlParser(Parser):
17 __slots__ = ()
18
19 content_type = ContentType.xml
20
21 @override
22 def parse(
23 self,
24 to_deserialize: Raw,
25 deserializer_hook: DeserializeFunc | None = None,
26 *,
27 request: HttpRequest,
28 model: Any,
29 ) -> Any:
30 try:
31 parsed = xmltodict.parse(
32 to_deserialize,
33 process_namespaces=True,
34 postprocessor=self._postprocessor,
35 # TODO: this is really bad, but I have no idea what to do.
36 force_list={'detail', 'loc'},
37 )
38 except expat.ExpatError as exc:
39 raise RequestSerializationError(str(exc)) from None
40 return parsed[next(iter(parsed.keys()))]
41
42 def _postprocessor(
43 self,
44 path: Any,
45 key: str,
46 xml_value: Any,
47 ) -> tuple[str, Any]:
48 # xmltodict converts empty tags to `None`; for leaf fields in payloads
49 # we normalize them to empty strings to match OpenAPI string semantics.
50 if xml_value is None:
51 return key, ''
52 return key, xml_value
53
54
55@final
56class XmlRenderer(Renderer):
57 __slots__ = ()
58
59 content_type = ContentType.xml
60
61 @override
62 def render(
63 self,
64 to_serialize: Any,
65 serializer_hook: Callable[[Any], Any],
66 ) -> bytes:
67 preprocessor = self._wrap_serializer(serializer_hook)
68 raw_data = xmltodict.unparse(
69 {type(to_serialize).__qualname__: to_serialize},
70 preprocessor=preprocessor,
71 )
72 assert isinstance(raw_data, str)
73 return raw_data.encode('utf8')
74
75 @property
76 @override
77 def validation_parser(self) -> XmlParser:
78 return XmlParser()
79
80 def _wrap_serializer(
81 self,
82 serializer_hook: Callable[[Any], Any],
83 ) -> Callable[[str, Any], tuple[str, Any]]:
84 def factory(xml_key: str, xml_value: Any) -> tuple[str, Any]:
85 try: # noqa: SIM105
86 xml_value = serializer_hook(xml_value)
87 except DataRenderingError:
88 pass # noqa: WPS420
89 return xml_key, xml_value
90
91 return factory
Warning
This parser is only used as a demo, do not use it in production, prefer more tested and battle-proven solutions.
Using different schemes for different content types¶
Sometimes we have to accept different schemas based on the content type.
According to the OpenAPI spec,
Body
should support different content types.
We utilize typing.Annotated
and dmr.negotiation.conditional_type():
1from typing import Annotated
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.negotiation import ContentType, conditional_type
7from dmr.plugins.msgspec import MsgspecJsonParser, MsgspecJsonRenderer
8from dmr.plugins.pydantic import PydanticSerializer
9from examples.negotiation.negotiation import XmlParser, XmlRenderer
10
11
12class _XMLRequestModel(pydantic.BaseModel):
13 root: dict[str, str]
14
15
16class ExampleController(
17 Controller[PydanticSerializer],
18):
19 parsers = (MsgspecJsonParser(), XmlParser())
20 renderers = (MsgspecJsonRenderer(), XmlRenderer())
21
22 def post(
23 self,
24 parsed_body: Body[
25 Annotated[
26 # The body will be a union of these two types:
27 _XMLRequestModel | dict[str, str],
28 conditional_type({
29 # But, for json it will always be:
30 ContentType.json: dict[str, str],
31 # And for xml it will always be:
32 ContentType.xml: _XMLRequestModel,
33 }),
34 ],
35 ],
36 ) -> dict[str, str]:
37 if isinstance(parsed_body, _XMLRequestModel):
38 return parsed_body.root
39 return parsed_body
40
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '<request><root><one>first</one></root></request>' -H 'Content-Type: application/xml' -H 'Accept: application/xml'
<?xml version="1.0" encoding="utf-8"?>
<dict><one>first</one></dict>
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"one": "first"}' -H 'Content-Type: application/json' -H 'Accept: application/json'
{"one":"first"}
$ curl http://127.0.0.1:8000/api/example/ -D - -X POST -d '{"root": {"mixin-json-content-type": "with-xml-format"}}' -H 'Content-Type: application/json' -H 'Accept: application/json'
HTTP/1.1 400 Bad Request
date: Thu, 07 May 2026 12:58:05 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 103
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Input should be a valid string","loc":["parsed_body","root"],"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"
},
"_XMLRequestModel": {
"properties": {
"root": {
"additionalProperties": {
"type": "string"
},
"title": "Root",
"type": "object"
}
},
"required": [
"root"
],
"title": "_XMLRequestModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/examplecontroller/": {
"post": {
"deprecated": false,
"operationId": "postExamplecontrollerApiExamplecontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_XMLRequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
"application/xml": {
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
We strictly validate that each content type will have its own unique model.
As the last example shows, it is impossible to send _XMLRequestModel
with Content-Type: application/json header.
The same works for return types as well:
1from typing import Annotated
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.negotiation import ContentType, conditional_type
7from dmr.plugins.msgspec import MsgspecJsonParser, MsgspecJsonRenderer
8from dmr.plugins.pydantic import PydanticSerializer
9from examples.negotiation.negotiation import XmlParser, XmlRenderer
10
11
12class _RequestModel(pydantic.BaseModel):
13 root: dict[str, str]
14
15
16class ExampleController(
17 Controller[PydanticSerializer],
18):
19 parsers = (MsgspecJsonParser(), XmlParser())
20 renderers = (MsgspecJsonRenderer(), XmlRenderer())
21
22 def post(
23 self,
24 parsed_body: Body[_RequestModel],
25 ) -> Annotated[
26 dict[str, str] | list[str],
27 conditional_type({
28 ContentType.json: list[str],
29 ContentType.xml: dict[str, str],
30 }),
31 ]:
32 if self.request.accepts(ContentType.json):
33 return list(parsed_body.root.values())
34 return parsed_body.root
35
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '<request><root><one>first</one></root></request>' -H 'Content-Type: application/xml' -H 'Accept: application/xml'
<?xml version="1.0" encoding="utf-8"?>
<dict><one>first</one></dict>
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"root": {"one": "first"}}' -H 'Content-Type: application/json' -H 'Accept: application/json'
["first"]
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": {
"root": {
"additionalProperties": {
"type": "string"
},
"title": "Root",
"type": "object"
}
},
"required": [
"root"
],
"title": "_RequestModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/examplecontroller/": {
"post": {
"deprecated": false,
"operationId": "postExamplecontrollerApiExamplecontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
},
"application/xml": {
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Depending on the content type - your return schema
will be fully validated as well.
In the example above, it would be an error to return something other
than list[str] for json content type, and it would also
be an error to return anything other than dict[str, str]
for xml content type.
You can combine conditional bodies and conditional return types in a type-safe and fully OpenAPI-compatible way.
Using different error models for different content types¶
The same can be done with error models. Let’s say you want to present JSON and XML error models differently.
We utilize the same technique typing.Annotated
and dmr.negotiation.conditional_type():
1from typing import Annotated, TypedDict
2
3import pydantic
4from typing_extensions import override
5
6from dmr import Body, Controller
7from dmr.errors import ErrorModel, ErrorType
8from dmr.negotiation import ContentType, conditional_type
9from dmr.plugins.msgspec import MsgspecJsonRenderer
10from dmr.plugins.pydantic import PydanticSerializer
11from examples.negotiation.negotiation import XmlRenderer
12
13
14class _RequestModel(pydantic.BaseModel):
15 root: dict[str, str]
16
17
18class _CustomXmlErrorModel(TypedDict):
19 xml_errors: dict[str, str]
20
21
22class ExampleController(
23 Controller[PydanticSerializer],
24):
25 renderers = (MsgspecJsonRenderer(), XmlRenderer())
26 error_model = Annotated[
27 ErrorModel | _CustomXmlErrorModel,
28 conditional_type({
29 ContentType.json: ErrorModel,
30 ContentType.xml: _CustomXmlErrorModel,
31 }),
32 ]
33
34 def post(self, parsed_body: Body[_RequestModel]) -> str:
35 # Will not be called in this example, because we fail to parse body:
36 raise NotImplementedError
37
38 @override
39 def format_error(
40 self,
41 error: str | Exception,
42 *,
43 loc: str | list[str | int] | None = None,
44 error_type: str | ErrorType | None = None,
45 ) -> ErrorModel | _CustomXmlErrorModel:
46 original: ErrorModel = super().format_error(
47 error,
48 loc=loc,
49 error_type=error_type,
50 )
51 if self.request.accepts(ContentType.json):
52 return original
53 return {
54 'xml_errors': {
55 '.'.join(str(location) for location in detail['loc']): detail[
56 'msg'
57 ]
58 for detail in original['detail']
59 },
60 }
61
Run result
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{}' -H 'Content-Type: application/json' -H 'Accept: application/json'
{"detail":[{"msg":"Field required","loc":["parsed_body","root"],"type":"value_error"}]}
$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{}' -H 'Content-Type: application/json' -H 'Accept: application/xml'
<?xml version="1.0" encoding="utf-8"?>
<dict><xml_errors><parsed_body.root>Field required</parsed_body.root></xml_errors></dict>
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"
},
"_CustomXmlErrorModel": {
"properties": {
"xml_errors": {
"additionalProperties": {
"type": "string"
},
"title": "Xml Errors",
"type": "object"
}
},
"required": [
"xml_errors"
],
"title": "_CustomXmlErrorModel",
"type": "object"
},
"_RequestModel": {
"properties": {
"root": {
"additionalProperties": {
"type": "string"
},
"title": "Root",
"type": "object"
}
},
"required": [
"root"
],
"title": "_RequestModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/examplecontroller/": {
"post": {
"deprecated": false,
"operationId": "postExamplecontrollerApiExamplecontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_RequestModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
},
"application/xml": {
"schema": {
"type": "string"
}
}
},
"description": "Created"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_CustomXmlErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_CustomXmlErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_CustomXmlErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Note that you would also have to customize
format_error()
accordingly.
Limiting response specs to content types¶
Sometimes some responses can only be returned for some content types. We need a way to describe it: both for our validation and OpenAPI spec.
To do so, we utilize limit_to_content_types
attribute:
1from http import HTTPStatus
2
3import pydantic
4
5from dmr import APIError, Controller, Query, ResponseSpec
6from dmr.negotiation import ContentType
7from dmr.plugins.msgspec import MsgspecJsonParser, MsgspecJsonRenderer
8from dmr.plugins.pydantic import PydanticSerializer
9from examples.negotiation.negotiation import XmlParser, XmlRenderer
10
11
12class _QueryModel(pydantic.BaseModel):
13 show_error: bool = False
14
15
16class ExampleController(Controller[PydanticSerializer]):
17 parsers = (MsgspecJsonParser(), XmlParser())
18 renderers = (MsgspecJsonRenderer(), XmlRenderer())
19 responses = (
20 ResponseSpec(
21 list[str],
22 status_code=HTTPStatus.CONFLICT,
23 limit_to_content_types={ContentType.json},
24 ),
25 ResponseSpec(
26 dict[str, str],
27 status_code=HTTPStatus.PAYMENT_REQUIRED,
28 limit_to_content_types={ContentType.xml},
29 ),
30 )
31
32 def get(self, parsed_query: Query[_QueryModel]) -> str:
33 if self.request.accepts(ContentType.json):
34 if parsed_query.show_error:
35 # This is explicitly wrong:
36 # `PAYMENT_REQUIRED` cannot happen with `json`,
37 # response validation will catch this:
38 raise APIError([], status_code=HTTPStatus.PAYMENT_REQUIRED)
39 raise APIError(['wrong', 'items'], status_code=HTTPStatus.CONFLICT)
40 raise APIError(
41 {'wrong': 'items'},
42 status_code=HTTPStatus.PAYMENT_REQUIRED,
43 )
44
Run result
$ curl http://127.0.0.1:8000/api/example/ -X GET -H 'Accept: application/json'
["wrong","items"]
$ curl http://127.0.0.1:8000/api/example/ -X GET -H 'Accept: application/xml'
<?xml version="1.0" encoding="utf-8"?>
<dict><wrong>items</wrong></dict>
$ curl 'http://127.0.0.1:8000/api/example/?show_error=1' -D - -X GET -H 'Accept: application/json'
HTTP/1.1 422 Unprocessable Entity
date: Thu, 07 May 2026 12:58:08 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 124
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Response 402 is not allowed for 'application/json', only for ['application/xml']","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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/examplecontroller/": {
"get": {
"deprecated": false,
"operationId": "getExamplecontrollerApiExamplecontroller",
"parameters": [
{
"deprecated": false,
"in": "query",
"name": "show_error",
"schema": {
"default": false,
"title": "Show Error",
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
},
"application/xml": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"402": {
"content": {
"application/xml": {
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
}
},
"description": "Payment Required"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"409": {
"content": {
"application/json": {
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
}
},
"description": "Conflict"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Disabling content negotiation validation¶
When validate_responses is active,
we also validate that the returned Content-Type header matches
the negotiated content type.
To disable this validation, if you for some reason,
want to break the content negotiation protocol,
you can set validate_negotiation
to False.
We support several layers of configuration:
1from http import HTTPStatus
2from typing import TypedDict
3
4from django.http import JsonResponse
5
6from dmr import Body, Controller, ResponseSpec, validate
7from dmr.plugins.msgspec import MsgspecSerializer
8from dmr.settings import default_parser, default_renderer
9from examples.negotiation.negotiation import XmlParser, XmlRenderer
10
11
12class _UserInputData(TypedDict):
13 email: str
14
15
16class UserController(Controller[MsgspecSerializer]):
17 parsers = (XmlParser(), default_parser)
18 renderers = (XmlRenderer(), default_renderer)
19
20 @validate(
21 ResponseSpec(_UserInputData, status_code=HTTPStatus.OK),
22 validate_negotiation=False,
23 )
24 def post(self, parsed_body: Body[_UserInputData]) -> JsonResponse:
25 # NOTE: do not do this!
26 return JsonResponse(parsed_body)
27
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '<request><email>user@example.com</email></request>' -H 'Content-Type: application/xml' -H 'Accept: application/xml'
{"email": "user@example.com"}
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"
},
"_UserInputData": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "_UserInputData",
"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/_UserInputData"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_UserInputData"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_UserInputData"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_UserInputData"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1from http import HTTPStatus
2from typing import TypedDict
3
4from django.http import JsonResponse
5
6from dmr import Body, Controller, ResponseSpec, validate
7from dmr.plugins.msgspec import MsgspecSerializer
8from dmr.settings import default_parser, default_renderer
9from examples.negotiation.negotiation import XmlParser, XmlRenderer
10
11
12class _UserInputData(TypedDict):
13 email: str
14
15
16class UserController(Controller[MsgspecSerializer]):
17 parsers = (XmlParser(), default_parser)
18 renderers = (XmlRenderer(), default_renderer)
19 validate_negotiation = False
20
21 @validate(ResponseSpec(_UserInputData, status_code=HTTPStatus.OK))
22 def post(self, parsed_body: Body[_UserInputData]) -> JsonResponse:
23 # NOTE: do not do this!
24 return JsonResponse(parsed_body)
25
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '<request><email>user@example.com</email></request>' -H 'Content-Type: application/xml' -H 'Accept: application/xml'
{"email": "user@example.com"}
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"
},
"_UserInputData": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "_UserInputData",
"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/_UserInputData"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_UserInputData"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_UserInputData"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/_UserInputData"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1from dmr.settings import Settings, default_parser, default_renderer
2from examples.negotiation.negotiation import XmlParser, XmlRenderer
3
4DMR_SETTINGS = { # noqa: WPS407
5 Settings.parsers: [XmlParser(), default_parser],
6 Settings.renderers: [XmlRenderer(), default_renderer],
7 Settings.validate_negotiation: False,
8}
Despite the fact that Content-Type is not validated,
we still validate the response schema. So, you must still provide correct
renderers and parsers to do validation. Otherwise, you would have to also
disable Response validation.
Warning
We do not ever recommend to do this in any sane setups.
This only makes sense for legacy API contracts that you want to migrate
to django-modern-rest.
Negotiation API¶
- class dmr.negotiation.RequestNegotiator(metadata: EndpointMetadata, serializer: type[BaseSerializer])[source]¶
Selects a correct parser type for a request.
- __call__(request: HttpRequest) Parser[source]¶
Negotiates which parser to use for parsing this request.
Based on
Content-Typeheader.Called in runtime. Must work for O(1) for the best case scenario because of that.
Must set
__dmr_parser__request attribute if the negotiation is successful.- Returns:
Parser class for this request.
- Raises:
RequestSerializationError – when
Content-Typerequest header is not supported.
- class dmr.negotiation.ResponseNegotiator(metadata: EndpointMetadata, serializer: type[BaseSerializer], *, streaming: bool)[source]¶
Selects a correct renderer for a response body.
Changed in version 0.5.0: Now it uses a custom algorithm that is x30 times faster (when compiled with mypyc compilation) then the original
django.http.HttpRequest.get_preferred_type()way we used before.- __call__(request: HttpRequest) Renderer[source]¶
Negotiates which renderer to use for rendering this response.
Based on
Acceptheader.Called in runtime. Must work for O(1) because of that.
We use
django.http.HttpRequest.get_preferred_type()inside. So, we have exactly the same negotiation rules as django has.Must set
__dmr_renderer__request attribute if the negotiation is successful. Can set__dmr_nonstreaming_renderer__if working with streaming responses.- Returns:
Renderer class for this response.
- Raises:
NotAcceptableError – when
Acceptrequest header is not supported.
- final class dmr.negotiation.ContentType(*values)[source]¶
Enumeration of frequently used content types.
- json¶
'application/json'format.
- xml¶
'application/xml'format.
- x_www_form_urlencoded¶
'application/x-www-form-urlencoded'format.
- multipart_form_data¶
'multipart/form-data'format.
- msgpack¶
'application/msgpack'format.
- event_stream¶
'text/event-stream'format for SSE streaming.
- jsonl¶
'application/jsonl'format for JSON Lines streaming.
- json_problem_details¶
'application/problem+json'format for RFC 9457.
- dmr.negotiation.conditional_type(mapping: Mapping[str | ContentType, Any]) ConditionalType[source]¶
Create conditional validation for different content types.
It is rather usual to see a requirement like: - If this method returns
jsonthen we should follow schema1 - If this methods returnsxmlthen we should follow schema2
- dmr.negotiation.request_parser(request: HttpRequest, *, strict: Literal[True]) Parser[source]¶
- dmr.negotiation.request_parser(request: HttpRequest, *, strict: bool = False) Parser | None
Get parser used to parse this request.
When strict is passed and request has no parser, we raise
AttributeError.Note
Since request parsing is only used when there’s a
dmr.components.Bodyor similar component, there might be no parser at all.
- dmr.negotiation.request_renderer(request: HttpRequest, *, strict: Literal[True], use_nonstreaming_renderer: bool = False) Renderer[source]¶
- dmr.negotiation.request_renderer(request: HttpRequest, *, strict: bool = False, use_nonstreaming_renderer: bool = False) Renderer | None
Get pre-negotiated renderer.
First, tries a special
__dmr_nonstreaming_renderer__case, which will be different forstreamingresponses. For example: for SSE controllers__dmr_nonstreaming_renderer__will be justjsonorxml. It is not used for REST endpoints.While
__dmr_renderer__will be whateverAcceptheader contains as the first value.When strict is passed and request has no renderer, we raise
AttributeError.Note
There might not be a response renderer that fits what client has asked. So, it can return
None.
Parser API¶
- class dmr.parsers.Parser[source]¶
Base class for all parsers.
Subclass it to implement your own parsers.
- abstractmethod parse(to_deserialize: bytes | bytearray, deserializer_hook: Callable[[type[Any], Any], Any] | None = None, *, request: HttpRequest, model: Any) Any[source]¶
Deserialize a raw string/bytes/bytearray into an object.
- Parameters:
to_deserialize – Value to deserialize.
deserializer_hook – Hook to convert types that are not natively supported.
request – Django’s original request with all the details.
model – Model that represents the final result’s structure.
- Returns:
Simple python object with primitive parts.
- Raises:
DataParsingError – If error decoding
obj.
- provide_response_specs(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], existing_responses: Mapping[HTTPStatus, ResponseSpec]) list[ResponseSpec][source]¶
Provides responses that can happen when data can’t be parsed.
Renderer API¶
- class dmr.renderers.Renderer[source]¶
Base class for all renderer types.
Subclass it to implement your own renderers.
- provide_response_specs(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], existing_responses: Mapping[HTTPStatus, ResponseSpec]) list[ResponseSpec][source]¶
Provides responses that can happen when data can’t be rendered.
Existing parsers and renderers¶
Parsers¶
- class dmr.plugins.msgspec.MsgspecJsonParser[source]¶
Parsers json bodies using
msgspec.- content_type: str = 'application/json'¶
Content-Type that this parser works with.
Must be defined for all subclasses.
- parse(to_deserialize: bytes | bytearray, deserializer_hook: Callable[[type[Any], Any], Any] | None = None, *, request: HttpRequest, model: Any) Any[source]¶
Deserialize a raw JSON string/bytes/bytearray into an object.
- Parameters:
to_deserialize – Value to deserialize.
deserializer_hook – Hook to convert types that are not natively supported.
request – Django’s original request with all the details.
model – Model that represents the final result’s structure.
- Returns:
Simple python object with primitive parts.
- Raises:
DataParsingError – If error decoding
obj.
- class dmr.plugins.msgspec.MsgpackParser[source]¶
Parsers
msgpackbodies usingmsgspec.- content_type: str = 'application/msgpack'¶
Content-Type that this parser works with.
Must be defined for all subclasses.
- parse(to_deserialize: bytes | bytearray, deserializer_hook: Callable[[type[Any], Any], Any] | None = None, *, request: HttpRequest, model: Any) Any[source]¶
Deserialize a raw msgpack string/bytes/bytearray into an object.
- Parameters:
to_deserialize – Value to deserialize.
deserializer_hook – Hook to convert types that are not natively supported.
request – Django’s original request with all the details.
model – Model that represents the final result’s structure.
- Returns:
Simple python object with primitive parts.
- Raises:
DataParsingError – If error decoding
obj.
- class dmr.parsers.JsonParser(content_type: str = 'application/json', *, json_module: JsonModule = <class 'dmr.internal.json.NativeJson'>)[source]¶
Fallback implementation of a json parser.
Only is used when
msgspecis not installed.Warning
It is not recommended to be used directly when using default
jsonmodule. It is slow and has less features. We won’t add any complex objects support to this parser.Alternative
jsonimplementations can be provided. See Alternative JSON backends for more info.- parse(to_deserialize: bytes | bytearray, deserializer_hook: Callable[[type[Any], Any], Any] | None = None, *, request: HttpRequest, model: Any) Any[source]¶
Decode a JSON string/bytes/bytearray into an object.
- Parameters:
to_deserialize – Value to decode.
deserializer_hook – Hook to convert types that are not natively supported.
request – Django’s original request with all the details.
model – Model that represents the final result’s structure.
- Returns:
Decoded object.
- Raises:
DataParsingError – If error decoding
obj.
- class dmr.parsers.MultiPartParser[source]¶
Parses multipart form data.
In reality this is a quite tricky parser. Since, Django already parses
multipart/form-datacontent natively, there’s no reason to duplicate its work. So, we return original Django’s content.
Renderers¶
- class dmr.plugins.msgspec.MsgspecJsonRenderer(content_type: str = 'application/json')[source]¶
Renders json bodies using
msgspec.- render(to_serialize: Any, serializer_hook: Callable[[Any], Any] | None = None) bytes[source]¶
Encode a value into JSON bytestring.
- Parameters:
to_serialize – Value to encode.
serializer_hook – Callable to support non-natively supported types.
- Returns:
JSON as bytes.
- property validation_parser: MsgspecJsonParser¶
Msgspec can parse this.
- class dmr.plugins.msgspec.MsgpackRenderer[source]¶
Renders
msgpackbodies usingmsgspec.- content_type: str = 'application/msgpack'¶
Content-Type that this renderer works with.
Must be defined for all subclasses.
- render(to_serialize: Any, serializer_hook: Callable[[Any], Any] | None = None) bytes[source]¶
Encode a value into
msgpackbytestring.- Parameters:
to_serialize – Value to encode.
serializer_hook – Callable to support non-natively supported types.
- Returns:
msgpackas bytes.
- property validation_parser: MsgpackParser¶
Msgspec can parse this.
- class dmr.renderers.JsonRenderer(content_type: str = 'application/json', *, json_module: JsonModule = <class 'dmr.internal.json.NativeJson'>)[source]¶
Fallback implementation of a json renderer.
Only is used when
msgspecis not installed.Warning
It is not recommended to be used directly. It is slow and has less features. We won’t add any complex objects support to this renderer.
Alternative
jsonimplementations can be provided. See Alternative JSON backends for more info.- render(to_serialize: Any, serializer_hook: Callable[[Any], Any]) bytes[source]¶
Encode a value into JSON bytestring.
- Parameters:
to_serialize – Value to encode.
serializer_hook – Callable to support non-natively supported types.
- Returns:
JSON as bytes.
- property validation_parser: JsonParser¶
Regular json parser can parse this.
- class dmr.renderers.FileRenderer(content_type: str = '*/*')[source]¶
Renders any file.
Works with any files and any content types.
Warning
Works with any content type by default, so it must be an only renderer for the endpoint.
- property validation_parser: _NoOpParser¶
Since there’s nothing to parse, we return a no-op.
Advanced API¶
- class dmr.parsers.SupportsFileParsing[source]¶
Mixin class for parsers that can parse files.
We require parsers that can parse files to populate
django.http.HttpRequest.FILESand to not return anything.
- class dmr.parsers.SupportsDjangoDefaultParsing[source]¶
Mark for parsers that support default Django’s parsing.
By default Django can parse multipart/form-data and application/x-www-form-urlencoded in a very specific way. Django only parses
django.http.HttpRequest.POSTanddjango.http.HttpRequest.FILESwhen it receives a realPOSTrequest. Which does not really work for us. We need more methods to be able to send the same content.So, parsers that extends this type must: 1. Return default parsed objects when method is
POST2. Parse similar HTTP methods the same way Django does forPOSTContract:
parse()method must returnNone, but populatedjango.http.HttpRequest.POSTanddjango.http.HttpRequest.FILESif they were missing.