Uploading files

There are several ways users can send files to a REST API:

  1. Via multipart/form-data requests. 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 our MultiPartParser to be used

  2. Via direct requests with a single file and a concrete content-type metadata

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

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:

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, 26 Apr 2026 21:11:14 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"
          }
        }
      }
    }
  }
}

What happens in this example?

  1. We define a FileMetadata model using msgspec.Struct or pydantic.BaseModel. Other types are also supported: typing.TypedDict, dataclasses.dataclass(), etc

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

  3. Then we use self.parsed_file_metadata that 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

See Customizing media types.

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:

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.

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 0x7a3ebd293e50>]

class dmr.components.FileMetadataComponent(schema_metadata: type[FileBody] = <class 'dmr.files.FileBody'>)[source]

Bases: ComponentParser

Parses 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.UploadedFile for the full list of metadata attributes.

Parameter for FileMetadata component must be named parsed_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 to frozenset of 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-data request 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.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_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.SupportsFileParsing instance to be present in parsers.

Runs in import time.