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: Sun, 26 Apr 2026 21:11:04 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: Sun, 26 Apr 2026 21:11:05 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: Sun, 26 Apr 2026 21:11:06 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: Sun, 26 Apr 2026 21:11:06 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 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: Sun, 26 Apr 2026 21:11:07 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: Sun, 26 Apr 2026 21:11:08 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: Sun, 26 Apr 2026 21:11:09 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 0x7a3ebe2f3700>]
- 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.