Request body#

The 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

The 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.

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: Mon, 05 Oct 2026 11:16: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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "put": {
        "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?

  1. We define a Body model using msgspec.Struct, pydantic.BaseModel, attrs.define(), typing.TypedDict, or dataclasses.dataclass(). Basically, the model definition is only limited by the BaseSerializer support

  2. Next, we use the Body component, provide the model as a type parameter, and subclass it when defining the Controller type

  3. Then we use self.parsed_body which 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 negotiation.

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.

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: Mon, 05 Oct 2026 11:16:42 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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "put": {
        "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"
          }
        }
      }
    }
  }
}

Optional body#

Request body can have a default value, in this case it is optional:

Run result

$ curl http://127.0.0.1:8000/api/products/ -X POST
"all"

$ curl http://127.0.0.1:8000/api/products/ -X POST -d '{"category": "cars"}' -H 'Content-Type: application/json'
"cars"

$ curl http://127.0.0.1:8000/api/products/ -D - -X POST -d '{"other": "cars"}' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
date: Mon, 05 Oct 2026 11:16:43 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 105
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":[{"msg":"Object missing required field `category` - 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"
      },
      "ProductFilters": {
        "properties": {
          "category": {
            "type": "string"
          }
        },
        "required": [
          "category"
        ],
        "title": "ProductFilters",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/productcontroller/": {
      "post": {
        "operationId": "postProductControllerApiProductcontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProductFilters"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "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"
          }
        }
      }
    }
  }
}

Requests without a body get parsed_body=None, such bodies are documented with required: false in the OpenAPI schema. Requests with a body are always validated, even when there’s a default.

See Default values to learn more.

Fast body parsing with msgspec#

Added in version 0.16.0.

Tip

BodyMsgspec is generally x1.6 faster than Body.

By default, Body is parsed in two steps:

  1. The Parser decodes the raw request bytes into simple python objects, like dict and list, using no exact shape

  2. Then, all components of the request (body, headers, query, etc) are validated together in a single call with the model that the BaseSerializer builds for the whole request, see Customizing the serializer context

This is flexible, but the intermediate python objects cost time. But, msgspec can decode bytes directly into a model, when it knows the shape in advance. It gives a lot of speed: for example, msgspec skips unknown fields and does not parse them from the body payload. It is a drop-in replacement for Body when using MsgspecSerializer:

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", "age": "not-a-number"}' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
date: Mon, 05 Oct 2026 11:16:45 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 82
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":[{"msg":"Expected `int`, got `str` - at `$.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"
                }
              ]
            },
            "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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "put": {
        "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 is different from Body?

  • Only MsgspecSerializer is supported, other serializers raise EndpointMetadataError during the import time. pydantic cannot benefit from this: its models can’t be decoded by msgspec, and validating them from within the decoder is slower than the regular Body in all measured cases

  • The body is validated on its own, before all other components are validated together. When the body is invalid, other components are not validated at all: the error response only contains body errors, even if parsed_headers or parsed_query are also invalid

  • Error locations change: the body is validated as the root object, not as a part of the whole request, so the parsed_body prefix is gone. Body reports $.parsed_body.age, BodyMsgspec reports $.age for the same error. Top level errors have no location at all

  • Invalid bytes, like malformed json, are still reported as parsing errors, exactly like for Body

  • Strictness of msgspec decoding is controlled by strict and strict attributes, which are lax by default, just like the regular request validation. strict_validation has no effect on fast bodies

Tip

BodyMsgspec works best if there are no other components.

Everything else works the same: defaults, conditional types, OpenAPI schema generation, and response validation. The OpenAPI schema is always the same for Body and BodyMsgspec.

Note

Only parsers that decode into models really support BodyMsgspec: MsgspecJsonParser and MsgpackParser.

Custom parsers can also learn to respect the model, but it needs a support from the core library’s serializer side.

Customizing the OpenAPI metadata for Body#

See Customizing media types.

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 libraries, or 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:

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: Mon, 05 Oct 2026 11:16:46 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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "put": {
        "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"
          }
        }
      }
    }
  }
}

Tip

If you are using Django 6.1+ and have a lot of form requests, you can swap the django.http.HttpRequest.multipart_parser_class attribute of request objects for faster parsing. We recommend using the multipart package for that. It works twice as fast.

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:

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "put": {
        "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:

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "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 when you 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.

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "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.

Conditional models#

django-modern-rest supports using different request models for different request content types.

See Using different schemes for different content types to learn more.

API Reference#

dmr.components.Body#

Annotated alias for parsing requests bodies.

alias of Annotated[_BodyT, <dmr.components.BodyComponent object at 0x7170bff25900>]

class dmr.components.BodyComponent(*, pass_model: bool = False)[source]#

Bases: ComponentParser

Parses 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} into UserCreateInput model.

Parameter for Body component must be named parsed_body.

When working with parsers that support dmr.parsers.SupportsDjangoDefaultParsing interface, you can specify __dmr_split_commas__ attribute: it must contain a frozenset of field aliases that will be split by ',' char.

See Using different schemes for different content types to learn more about conditional bodies.

Parameters:

pass_model – Whether or not to pass model to the serializer or Any. Plugins that support direct model parsing provide their own aliases, like BodyMsgspec, and validate that their serializer is used to parse objects faster. See Fast body parsing with msgspec to learn more.

Changed in version 0.16.0: Added pass_model parameter.

conditional_types(model: TypeForm[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.Annotated is passed together with dmr.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: TypeForm[Any], model_meta: tuple[Any, ...], metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], context: OpenAPIContext) → list[Parameter | Reference] | RequestBody[source]#

Generate OpenAPI spec for component.

Changed in version 0.16.0: serializer parameter was changed to be controller_cls.

provide_context_data(endpoint: Endpoint, controller: Controller[BaseSerializer], *, field_model: TypeForm[Any], default: Any = EMPTY) → 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.

default is the default value of the endpoint parameter, like parsed_body: Body[Model | None] = None. When it is not EMPTY and the request has no data for this component, return default as-is.

Changed in version 0.16.0: Added default parameter.