Uploading files¶
There are several ways users can send files to a REST API:
Via
multipart/form-datarequests. It supports passing multiple files at once, it also supports sending other body parameters together with the files. It is the best option for 95% of cases. This way requires ourMultiPartParserto be usedVia direct requests with a single file and a concrete content-type metadata
Via base64-encoded strings inside a JSON or XML files. It is only suitable for really small files
Currently we support only the first option.
The second option is not supported yet, but it may be supported in future releases.
Currently users can implement their own Parser to do that.
While the third way has no specific support, but it can be implemented
by users directly.
Danger
Uploading files with Python and Django can be really slow. For most cases it would be a better idea to use S3-like system to upload user-generated content.
Sync uploads must never be used. Even a very small amount of traffic will completely block your app.
Parsing files¶
Note
Parsed FileMetadata parameter must be named parsed_file_metadata.
While file objects themselves are available as self.request.FILES.
See django.http.HttpRequest.FILES for more info.
We don’t provide any extra abstractions on top of Django’s file uploads.
Note
Official Django docs: https://docs.djangoproject.com/en/stable/topics/http/file-uploads/
All Django features for file uploads work as well for django-modern-rest.
Like FILE_UPLOAD_MAX_MEMORY_SIZE
or FILE_UPLOAD_HANDLERS.
So, we do not touch Django’s internal logic for file uploads. What we do instead is: we provide extra metadata to be validated / rendered in the schema:
1from typing import Literal
2
3import msgspec
4
5from dmr import Controller, FileMetadata
6from dmr.parsers import MultiPartParser
7from dmr.plugins.msgspec import MsgspecSerializer
8
9
10class _FileModel(msgspec.Struct):
11 # No other content types will be allowed:
12 content_type: Literal['text/plain']
13 # No other filenames will be allowed:
14 name: Literal['receipt.txt', 'rules.txt']
15
16
17class _UploadedFiles(msgspec.Struct):
18 receipt: _FileModel
19 rules: _FileModel
20
21
22class FileController(Controller[MsgspecSerializer]):
23 parsers = (MultiPartParser(),)
24
25 def put(
26 self,
27 parsed_file_metadata: FileMetadata[_UploadedFiles],
28 ) -> _UploadedFiles:
29 return parsed_file_metadata
30
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT -F receipt=@receipt.txt -F rules=@rules.txt -H 'Content-Type: multipart/form-data'
{"receipt":{"content_type":"text/plain","name":"receipt.txt"},"rules":{"content_type":"text/plain","name":"rules.txt"}}
$ curl http://127.0.0.1:8000/api/users/ -D - -X PUT -F receipt=@wrong.json -H 'Content-Type: multipart/form-data'
HTTP/1.1 400 Bad Request
date: Sun, 05 Apr 2026 17:50:45 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 118
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Invalid enum value 'wrong.json' - at `$.parsed_file_metadata.receipt.name`","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"
},
"_FileModel": {
"properties": {
"content_type": {
"enum": [
"text/plain"
]
},
"name": {
"enum": [
"receipt.txt",
"rules.txt"
]
}
},
"required": [
"content_type",
"name"
],
"title": "_FileModel",
"type": "object"
},
"_UploadedFiles": {
"properties": {
"receipt": {
"$ref": "#/components/schemas/_FileModel"
},
"rules": {
"$ref": "#/components/schemas/_FileModel"
}
},
"required": [
"receipt",
"rules"
],
"title": "_UploadedFiles",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/filecontroller/": {
"put": {
"deprecated": false,
"operationId": "putFilecontrollerApiFilecontroller",
"requestBody": {
"content": {
"multipart/form-data": {
"encoding": {
"receipt": {
"contentType": "text/plain"
},
"rules": {
"contentType": "text/plain"
}
},
"schema": {
"properties": {
"receipt": {
"format": "binary",
"type": "string"
},
"rules": {
"format": "binary",
"type": "string"
}
},
"required": [
"receipt",
"rules"
],
"title": "_UploadedFiles",
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_UploadedFiles"
}
}
},
"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"
}
}
}
}
}
}
1from typing import Literal
2
3import pydantic
4
5from dmr import Controller, FileMetadata
6from dmr.parsers import MultiPartParser
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class _FileModel(pydantic.BaseModel):
11 # No other content types will be allowed:
12 content_type: Literal['text/plain']
13 # No other filenames will be allowed:
14 name: Literal['receipt.txt', 'rules.txt']
15
16
17class _UploadedFiles(pydantic.BaseModel):
18 receipt: _FileModel
19 rules: _FileModel
20
21
22class FileController(Controller[PydanticSerializer]):
23 parsers = (MultiPartParser(),)
24
25 def put(
26 self,
27 parsed_file_metadata: FileMetadata[_UploadedFiles],
28 ) -> _UploadedFiles:
29 return parsed_file_metadata
30
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT -F receipt=@receipt.txt -F rules=@rules.txt -H 'Content-Type: multipart/form-data'
{"receipt":{"content_type":"text/plain","name":"receipt.txt"},"rules":{"content_type":"text/plain","name":"rules.txt"}}
$ curl http://127.0.0.1:8000/api/users/ -D - -X PUT -F receipt=@wrong.json -H 'Content-Type: multipart/form-data'
HTTP/1.1 400 Bad Request
date: Sun, 05 Apr 2026 17:50:46 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 337
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Input should be 'text/plain'","loc":["parsed_file_metadata","receipt","content_type"],"type":"value_error"},{"msg":"Input should be 'receipt.txt' or 'rules.txt'","loc":["parsed_file_metadata","receipt","name"],"type":"value_error"},{"msg":"Field required","loc":["parsed_file_metadata","rules"],"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"
},
"_FileModel": {
"properties": {
"content_type": {
"const": "text/plain",
"title": "Content Type",
"type": "string"
},
"name": {
"enum": [
"receipt.txt",
"rules.txt"
],
"title": "Name",
"type": "string"
}
},
"required": [
"content_type",
"name"
],
"title": "_FileModel",
"type": "object"
},
"_UploadedFiles": {
"properties": {
"receipt": {
"$ref": "#/components/schemas/_FileModel"
},
"rules": {
"$ref": "#/components/schemas/_FileModel"
}
},
"required": [
"receipt",
"rules"
],
"title": "_UploadedFiles",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/filecontroller/": {
"put": {
"deprecated": false,
"operationId": "putFilecontrollerApiFilecontroller",
"requestBody": {
"content": {
"multipart/form-data": {
"encoding": {
"receipt": {
"contentType": "text/plain"
},
"rules": {
"contentType": "text/plain"
}
},
"schema": {
"properties": {
"receipt": {
"format": "binary",
"type": "string"
},
"rules": {
"format": "binary",
"type": "string"
}
},
"required": [
"receipt",
"rules"
],
"title": "_UploadedFiles",
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_UploadedFiles"
}
}
},
"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
FileMetadatamodel usingmsgspec.Structorpydantic.BaseModel. Other types are also supported:typing.TypedDict,dataclasses.dataclass(), etcNext, we use
FileMetadatacomponent, provide the model as a type parameter, and subclass it when definingControllertypeThen we use
self.parsed_file_metadatathat will have the correct model type
Note
Unlike raw Django, django-modern-rest allows file uploads
for all HTTP methods with defined bodies.
Here’s the list of fields that we support as a metadata:
[
'size',
'name',
'content_type',
'charset',
'content_type_extra',
]
We don’t copy content for the validation, only metadata.
Customizing OpenAPI metadata for FileMetadata¶
Sending files with extra body parameters¶
It might be required to send some files as multipart/form-data
together with some extra information, like user_id.
This is also supported:
1from typing import Literal
2
3import pydantic
4from django.core.files.uploadedfile import UploadedFile
5
6from dmr import Body, Controller, FileMetadata
7from dmr.parsers import MultiPartParser
8from dmr.plugins.pydantic import PydanticSerializer
9
10
11class _FileModel(pydantic.BaseModel):
12 content_type: Literal['text/plain']
13 size: int
14
15
16class _UploadedFiles(pydantic.BaseModel):
17 receipt: _FileModel
18 rules: _FileModel
19
20
21class _BodyPayload(pydantic.BaseModel):
22 user_id: int
23 user_email: str
24
25
26class _OutputPayload(pydantic.BaseModel):
27 receipt: _FileModel
28 rules: _FileModel
29 user_id: int
30 user_email: str
31
32
33class FileAndBodyController(Controller[PydanticSerializer]):
34 parsers = (MultiPartParser(),)
35
36 def post(
37 self,
38 parsed_body: Body[_BodyPayload],
39 parsed_file_metadata: FileMetadata[_UploadedFiles],
40 ) -> _OutputPayload:
41 for content_key in parsed_file_metadata.model_fields_set:
42 assert isinstance(self.request.FILES[content_key], UploadedFile)
43 return _OutputPayload(
44 user_id=parsed_body.user_id,
45 user_email=parsed_body.user_email,
46 receipt=parsed_file_metadata.receipt,
47 rules=parsed_file_metadata.rules,
48 )
49
Run result
$ curl http://127.0.0.1:8000/api/users/ -X POST -F user_id=1 -F user_email=example@mail.com -F receipt=@receipt.txt -F rules=@rules.txt -H 'Content-Type: multipart/form-data'
{"receipt":{"content_type":"text/plain","size":16},"rules":{"content_type":"text/plain","size":14},"user_id":1,"user_email":"example@mail.com"}
To do that also define Body component
in the same controller.
Sending files with json as a body parameter¶
You can send complex data together with files as multipart/form-data.
To do so, you would need to encode json as a string
and attach it to a form data field.
1from typing import Literal
2
3import pydantic
4
5from dmr import Body, Controller, FileMetadata
6from dmr.parsers import MultiPartParser
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class _FileModel(pydantic.BaseModel):
11 content_type: Literal['text/plain']
12 size: int
13
14
15class _UploadedFiles(pydantic.BaseModel):
16 receipt: _FileModel
17 rules: _FileModel
18
19
20class _BodyPayload(pydantic.BaseModel):
21 user_id: int
22 user_email: str
23
24
25class _JsonBodyPayload(pydantic.BaseModel):
26 user: pydantic.Json[_BodyPayload]
27
28
29class _JsonOutputPayload(pydantic.BaseModel):
30 receipt: _FileModel
31 rules: _FileModel
32 user: _BodyPayload
33
34
35class FileAndJsonController(Controller[PydanticSerializer]):
36 parsers = (MultiPartParser(),)
37
38 def put(
39 self,
40 parsed_body: Body[_JsonBodyPayload],
41 parsed_file_metadata: FileMetadata[_UploadedFiles],
42 ) -> _JsonOutputPayload:
43 return _JsonOutputPayload(
44 user=parsed_body.user,
45 receipt=parsed_file_metadata.receipt,
46 rules=parsed_file_metadata.rules,
47 )
48
Run result
$ curl http://127.0.0.1:8000/api/users/ -X PUT -F 'user={"user_id": 1, "user_email": "example@mail.com"}' -F receipt=@receipt.txt -F rules=@rules.txt -H 'Content-Type: multipart/form-data'
{"receipt":{"content_type":"text/plain","size":16},"rules":{"content_type":"text/plain","size":14},"user":{"user_id":1,"user_email":"example@mail.com"}}
The easiest way to do this would be to declare
a field as pydantic.Json.
However, this can be done with msgspec as well.
API Reference¶
- dmr.components.FileMetadata¶
Annotated alias for parsing file metadata.
alias of
Annotated[_FileMetadataT, <dmr.components.FileMetadataComponent object at 0x7ff62a6679d0>]
- class dmr.components.FileMetadataComponent(schema_metadata: type[FileBody] = <class 'dmr.files.FileBody'>)[source]¶
Bases:
ComponentParserParses files metadata from
django.http.HttpRequest.FILES.Django handles files itself natively, we don’t need to do anything in
django-modern-rest. Everything just works, including all Django’s advanced file features like customizing the storage backends.But, we need a way to represent and validate the metadata.
This class is designed to do just that: validate files’ metadata.
>>> from typing import Literal >>> import pydantic >>> from dmr import Controller, FileMetadata >>> from dmr.plugins.pydantic import PydanticSerializer >>> from dmr.parsers import MultiPartParser >>> class TextFile(pydantic.BaseModel): ... # Will validate that all files are text files ... # and are less than 1000 bytes in size: ... name: str ... content_type: Literal['text/plain'] ... size: int = pydantic.Field(lt=1000) >>> class ContractPayload(pydantic.BaseModel): ... receipt: TextFile ... contract: TextFile >>> class ContractController(Controller[PydanticSerializer]): ... parsers = (MultiPartParser(),) ... ... def post( ... self, parsed_file_metadata: FileMetadata[ContractPayload] ... ) -> str: ... return 'Valid files!'
What attributes are available to be validated? See
django.core.files.uploadedfile.UploadedFilefor the full list of metadata attributes.Parameter for
FileMetadatacomponent must be namedparsed_file_metadata.Users can customize how they want their file metadata values: as single values or as lists of values. To do so, use
__dmr_force_list__optional attribute. Set it tofrozensetof file keys that need to be lists. All other values will be regular single values:>>> class ContractPayload(pydantic.BaseModel): ... __dmr_force_list__: ClassVar[frozenset[str]] = frozenset(( ... 'receipts', ... )) ... ... receipts: list[TextFile] ... contract: TextFile
This will parse a
multipart/form-datarequest with potentially multiple receipts and a single contract files.- 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_file_metadata'¶
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) Mapping[str, 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.
- validate(controller_cls: type[Controller[BaseSerializer]], metadata: EndpointMetadata) None[source]¶
Validates that the component is correctly defined.
This component requires at least one
dmr.parsers.SupportsFileParsinginstance to be present in parsers.Runs in import time.