Request body#
Body can be anything: json, xml,
application/x-www-form-urlencoded,
or multipart/form-data.
It depends on the Parser
that is being used for the endpoint.
Note
Parsed Body parameter must be named parsed_body.
Parsing JSON#
Here’s how you can parse Body with a model:
We support msgspec.Struct
via MsgspecSerializer.
1import msgspec
2
3from dmr import Body, Controller
4from dmr.plugins.msgspec import MsgspecSerializer
5
6
7class _User(msgspec.Struct):
8 username: str
9 age: int
10
11
12class UserController(Controller[MsgspecSerializer]):
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: Thu, 16 Jul 2026 09:34:37 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 100
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Object missing required field `age` - at `$.parsed_body`","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"
}
]
},
"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"
},
"_User": {
"properties": {
"age": {
"type": "integer"
},
"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"
}
}
}
}
}
}
We support pydantic.BaseModel
via PydanticSerializer.
1import pydantic
2
3from dmr import Body, Controller
4from dmr.plugins.pydantic import PydanticSerializer
5
6
7class _User(pydantic.BaseModel):
8 username: str
9 age: int
10
11
12class UserController(Controller[PydanticSerializer]):
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: Thu, 16 Jul 2026 09:34:38 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"
}
}
}
}
}
}
We support attrs.define()
via MsgspecSerializer.
1import attrs
2
3from dmr import Body, Controller
4from dmr.plugins.msgspec import MsgspecSerializer
5
6
7@attrs.define
8class _User:
9 username: str
10 age: int
11
12
13class UserController(Controller[MsgspecSerializer]):
14 def put(self, parsed_body: Body[_User]) -> _User:
15 return parsed_body
16
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT -d '{"username": "sobolevn", "age": 27}' -H 'Content-Type: application/json'
{"age":27,"username":"sobolevn"}
$ 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: Thu, 16 Jul 2026 09:34:39 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 100
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Object missing required field `age` - at `$.parsed_body`","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"
}
]
},
"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"
},
"_User": {
"properties": {
"age": {
"type": "integer"
},
"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"
}
}
}
}
}
}
We support dataclasses.dataclass() via both
MsgspecSerializer
and PydanticSerializer.
1import dataclasses
2
3from dmr import Body, Controller
4from dmr.plugins.msgspec import MsgspecSerializer
5
6
7@dataclasses.dataclass
8class _User:
9 username: str
10 age: int
11
12
13class UserController(Controller[MsgspecSerializer]):
14 def put(self, parsed_body: Body[_User]) -> _User:
15 return parsed_body
16
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: Thu, 16 Jul 2026 09:34:39 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 100
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Object missing required field `age` - at `$.parsed_body`","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"
}
]
},
"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"
},
"_User": {
"properties": {
"age": {
"type": "integer"
},
"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"
}
}
}
}
}
}
We support typing.TypedDict via both
MsgspecSerializer
and PydanticSerializer.
1from typing_extensions import TypedDict
2
3from dmr import Body, Controller
4from dmr.plugins.msgspec import MsgspecSerializer
5
6
7class _User(TypedDict):
8 username: str
9 age: int
10
11
12class UserController(Controller[MsgspecSerializer]):
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: Thu, 16 Jul 2026 09:34:40 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 100
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Object missing required field `age` - at `$.parsed_body`","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"
}
]
},
"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"
},
"_User": {
"properties": {
"age": {
"type": "integer"
},
"username": {
"type": "string"
}
},
"required": [
"age",
"username"
],
"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"
}
}
}
}
}
}
What happens in this example?
We define a
Bodymodel usingmsgspec.Struct,pydantic.BaseModel,attrs.define(),typing.TypedDict, ordataclasses.dataclass(). Basically, model definition is only limited by theBaseSerializersupportNext, we use
Bodycomponent, provide the model as a type parameter, and subclass it when definingControllertypeThen we use
self.parsed_bodythat will have the correct model type
Parsing MsgPack#
Note
This feature requires 'django-modern-rest[msgpack]' to be installed.
MsgPack is a binary, compact and really fast format for modern APIs. Docs: https://msgpack.org
Bodies can be parsed using different dmr.parsers.Parser types.
See our Content negotiation guide on more information
about content negotiations.
Here’s how msgpack will represent {"username": "example", "age": 22}
(since it is a binary format, it will show some random unicode symbols:
The only visible difference from parsing JSON is specifying a different
parsers instance.
1import msgspec
2
3from dmr import Body, Controller
4from dmr.plugins.msgspec import MsgpackParser, MsgspecSerializer
5
6
7class _User(msgspec.Struct):
8 username: str
9 age: int
10
11
12class UserController(Controller[MsgspecSerializer]):
13 parsers = (MsgpackParser(),)
14
15 def put(self, parsed_body: Body[_User]) -> _User:
16 return parsed_body
17
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT --data-binary @examples/components/body.msgpack -H 'Content-Type: application/msgpack'
{"username":"example","age":22}
$ curl http://127.0.0.1:8000/api/users/ -D - -X PUT --data-binary @examples/components/body_wrong.msgpack -H 'Content-Type: application/msgpack'
HTTP/1.1 400 Bad Request
date: Thu, 16 Jul 2026 09:34:41 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 100
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Object missing required field `age` - at `$.parsed_body`","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"
}
]
},
"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"
},
"_User": {
"properties": {
"age": {
"type": "integer"
},
"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/msgpack": {
"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"
}
}
}
}
}
}
Customizing OpenAPI metadata for Body#
Parsing forms#
Note
We don’t recommend using forms. If you can avoid using this feature and switch to json – you totally should.
Forms are only needed for compatibility with older APIs, strange libs, existing workflows.
Here’s an example how one can send application/x-www-form-urlencoded
form data to an API endpoint with the help
of FormUrlEncodedParser:
1import pydantic
2
3from dmr import Body, Controller
4from dmr.parsers import FormUrlEncodedParser
5from dmr.plugins.pydantic import PydanticSerializer
6
7
8class _User(pydantic.BaseModel):
9 username: str
10 age: int
11
12
13class UserController(Controller[PydanticSerializer]):
14 parsers = (FormUrlEncodedParser(),)
15
16 def put(self, parsed_body: Body[_User]) -> _User:
17 return parsed_body
18
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT -d 'username=sobolevn&age=27' -H 'Content-Type: application/x-www-form-urlencoded'
{"username":"sobolevn","age":27}
$ curl http://127.0.0.1:8000/api/users/ -D - -X PUT -d 'username=sobolevn&age=wrong' -H 'Content-Type: application/x-www-form-urlencoded'
HTTP/1.1 400 Bad Request
date: Thu, 16 Jul 2026 09:34:42 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 141
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Input should be a valid integer, unable to parse string as an integer","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/x-www-form-urlencoded": {
"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"
}
}
}
}
}
}
Forcing lists and casting nulls in forms#
Warning
All of the features below only work for
application/x-www-form-urlencoded and multipart/form-data
parsers. Json and other “modern” formats are not affected.
Django’s form parsing algorithm is 20+ years old at the moment of writing this doc.
There are some known quirks to it.
Forcing lists#
Django uses django.utils.datastructures.MultiValueDict
to store body data, when parsing forms. Due to its API,
it does not give list objects back easily.
So, when we need a list for a field, we need to force it like this:
1from typing import ClassVar
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.parsers import MultiPartParser
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class _User(pydantic.BaseModel):
11 __dmr_force_list__: ClassVar[frozenset[str]] = frozenset(('tags',))
12
13 username: str
14 tags: list[str]
15
16
17class UserController(Controller[PydanticSerializer]):
18 parsers = (MultiPartParser(),)
19
20 def put(self, parsed_body: Body[_User]) -> _User:
21 return parsed_body
22
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT -F username=sobolevn -F tags=python -F tags=django -H 'Content-Type: multipart/form-data'
{"username":"sobolevn","tags":["python","django"]}
$ curl http://127.0.0.1:8000/api/users/ -X PUT -F username=sobolevn -F tags=single -H 'Content-Type: multipart/form-data'
{"username":"sobolevn","tags":["single"]}
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": {
"tags": {
"items": {
"type": "string"
},
"title": "Tags",
"type": "array"
},
"username": {
"title": "Username",
"type": "string"
}
},
"required": [
"username",
"tags"
],
"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": {
"multipart/form-data": {
"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"
}
}
}
}
}
}
Split commas#
Another problem that might happen is that some field might
look like {'foo': 'bar,baz'}, not {'foo': ['bar', 'baz']}.
To solve this, one can use a different magic attribute:
1from typing import ClassVar
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.parsers import MultiPartParser
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class _User(pydantic.BaseModel):
11 __dmr_split_commas__: ClassVar[frozenset[str]] = frozenset(('tags',))
12
13 username: str
14 tags: list[str]
15
16
17class UserController(Controller[PydanticSerializer]):
18 parsers = (MultiPartParser(),)
19
20 def post(self, parsed_body: Body[_User]) -> _User:
21 return parsed_body
22
Run result
$ curl http://127.0.0.1:8000/api/users/ -X POST -F username=sobolevn -F tags=python,django -H 'Content-Type: multipart/form-data'
{"username":"sobolevn","tags":["python","django"]}
$ curl http://127.0.0.1:8000/api/users/ -X POST -F username=sobolevn -F tags=single -H 'Content-Type: multipart/form-data'
{"username":"sobolevn","tags":["single"]}
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": {
"tags": {
"items": {
"type": "string"
},
"title": "Tags",
"type": "array"
},
"username": {
"title": "Username",
"type": "string"
}
},
"required": [
"username",
"tags"
],
"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": {
"multipart/form-data": {
"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"
}
}
}
}
}
}
Warning
We split all data by ',', if your data contains ',' as a regular
value, it might be corrupted.
Be careful to use this with fields which do not contain ','.
Like list of ints, uuids, or slugs.
Casting nulls#
It is hard to pass None as a value in a form.
To solve the need for None many places offer to pass 'null' as a string.
We can cast 'null' back to None if __dmr_cast_null__ is specified.
1from typing import ClassVar
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.parsers import MultiPartParser
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class _User(pydantic.BaseModel):
11 __dmr_force_list__: ClassVar[frozenset[str]] = frozenset(('tags',))
12 __dmr_cast_null__: ClassVar[frozenset[str]] = frozenset((
13 'promo_id',
14 'tags',
15 ))
16
17 username: str
18 promo_id: int | None
19 tags: list[str | None]
20
21
22class UserController(Controller[PydanticSerializer]):
23 parsers = (MultiPartParser(),)
24
25 def post(self, parsed_body: Body[_User]) -> _User:
26 return parsed_body
27
Run result
$ curl http://127.0.0.1:8000/api/users/ -X POST -F username=sobolevn -F promo_id=1 -F tags=python -F tags=django -H 'Content-Type: multipart/form-data'
{"username":"sobolevn","promo_id":1,"tags":["python","django"]}
$ curl http://127.0.0.1:8000/api/users/ -X POST -F username=sobolevn -F promo_id=null -F tags=python -F tags=null -H 'Content-Type: multipart/form-data'
{"username":"sobolevn","promo_id":null,"tags":["python",null]}
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": {
"promo_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Promo Id"
},
"tags": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"title": "Tags",
"type": "array"
},
"username": {
"title": "Username",
"type": "string"
}
},
"required": [
"username",
"promo_id",
"tags"
],
"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": {
"multipart/form-data": {
"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"
}
}
}
}
}
}
You can combine this feature with
both __dmr_split_commas__ and __dmr_force_list__ as well.
API Reference#
- dmr.components.Body#
Annotated alias for parsing requests bodies.
alias of
Annotated[_BodyT, <dmr.components.BodyComponent object at 0x7607736dde80>]
- class dmr.components.BodyComponent[source]#
Bases:
ComponentParserParses body of the request.
For example:
>>> import pydantic >>> from dmr import Body, Controller >>> from dmr.plugins.pydantic import PydanticSerializer >>> class UserCreateInput(pydantic.BaseModel): ... email: str ... age: int >>> class UserCreateController(Controller[PydanticSerializer]): ... def post(self, parsed_body: Body[UserCreateInput]) -> str: ... return parsed_body.email
Will parse a body like
{'email': 'user@example.org', 'age': 18}intoUserCreateInputmodel.Parameter for
Bodycomponent must be namedparsed_body.When working with parsers that support
dmr.parsers.SupportsDjangoDefaultParsinginterface, you can specify__dmr_split_commas__attribute: it must contain afrozensetof field aliases that will be split by','char.- conditional_types(model: Any, model_meta: tuple[Any, ...]) Mapping[str, Any][source]#
Provide conditional parsing types based on content type.
Body model can be conditional based on a content_type. If
typing.Annotatedis passed together withdmr.negotiation.conditional_type()we treat the body as conditional. Otherwise, returns an empty dict.
- context_name: ClassVar[str] = 'parsed_body'#
All subtypes must provide a unique name that will be used to parse context.
We use a single context for all parsing, this component will live under a dict field with this name.
- get_schema(model: Any, model_meta: tuple[Any, ...], metadata: EndpointMetadata, serializer: type[BaseSerializer], context: OpenAPIContext) list[Parameter | Reference] | RequestBody[source]#
Generate OpenAPI spec for component.
- provide_context_data(endpoint: Endpoint, controller: Controller[BaseSerializer], *, field_model: Any) Any[source]#
Return unstructured raw values for
serializer.from_python().It must return the same number of elements that has type vars. Basically, each type var is a model. Each element in a tuple is the corresponding data for that model.
When this method returns not a tuple and there’s only one type variable, it also works.