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 usedjango.http.HttpRequest.get_preferred_type()method to match the best accepted type, based on'specificity', 'quality', 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.
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>abc4667b-71a2-4b1d-8205-b1a5597f7db5</uid></user></_UserDocument>
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":"a195a152-dcc5-48c0-925f-1dd859df88f8"}}
1from dmr.settings import Settings
2from examples.negotiation.negotiation import XmlParser, XmlRenderer
3
4DMR_SETTINGS = { # noqa: WPS407
5 # You can also use string fully qualified names to import them:
6 Settings.parsers: [XmlParser()],
7 Settings.renderers: [XmlRenderer()],
8}
First parsers / renders 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 schemes 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: Sun, 29 Mar 2026 18:52:56 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"}]}
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"]
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>
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: Sun, 29 Mar 2026 18:52:59 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"}]}
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.
- __call__(request: HttpRequest) Renderer[source]¶
Negotiates which parser to use for parsing this request.
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.
- dmr.negotiation.conditional_type(mapping: Mapping[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) Parser | None[source]¶
Get parser used to parse this request.
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, *, use_nonstreaming_renderer: bool = False) Renderer | None[source]¶
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.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 reprensents the final result’s structure.
- Returns:
Simple python object with primitive parts.
- Raises:
DataParsingError – If error decoding
obj.
- classmethod 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.
- classmethod 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 reprensents 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 reprensents the final result’s structure.
- Returns:
Simple python object with primitive parts.
- Raises:
DataParsingError – If error decoding
obj.
- class dmr.parsers.JsonParser[source]¶
Fallback implementation of a json parser.
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 parser.
- 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 reprensents the final result’s structure.
- Returns:
Decoded object.
- Raises:
DataParsingError – If error decoding
obj.
- class dmr.parsers.MultiPartParser[source]¶
Parses multipart form data.
In reallity 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[source]¶
Renders json bodies using
msgspec.- content_type: str = 'application/json'¶
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 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(encoder_cls: type[DjangoJSONEncoder] = <class 'dmr.renderers._DMREncoder'>)[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.
- 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.