Skip to content

File Uploads

To receive an uploaded file, annotate a parameter with UploadedFile. Django Ninja reads it from request.FILES, and — because a file can only travel over HTTP as multipart/form-data — automatically switches the operation's request encoding to multipart/form-data the moment it sees a File parameter (combined with Form fields or a JSON body, if there are any).

Basic usage

from ninja import NinjaAPI, UploadedFile

api = NinjaAPI()


@api.post("/upload")
def upload(request, file: UploadedFile):
    data = file.read()
    return {"name": file.name, "len": len(data)}

You don't need to mark the parameter with File(...) — Django Ninja recognizes the UploadedFile type on its own and treats it as a file parameter automatically. Marking it explicitly with File works exactly the same way and is useful once you need validation constraints:

from ninja import File, NinjaAPI, UploadedFile

api = NinjaAPI()


@api.post("/upload")
def upload(request, file: File[UploadedFile]):
    data = file.read()
    return {"name": file.name, "len": len(data)}

UploadedFile is Django's own UploadedFile (Django Ninja re-exports it so Pydantic knows how to validate it), so it has all of the usual attributes and methods:

  • read(size=None)
  • multiple_chunks(chunk_size=None)
  • chunks(chunk_size=None)
  • name
  • size
  • content_type
  • content_type_extra
  • charset

Multiple files

Declare a list of UploadedFile to accept several files under the same field name:

from ninja import NinjaAPI, UploadedFile

api = NinjaAPI()


@api.post("/upload-many")
def upload_many(request, files: list[UploadedFile]):
    return [f.name for f in files]

A client sends this as several multipart/form-data parts that all share the files field name. set and tuple annotations work the same way.

You can also just declare several separate UploadedFile parameters — each one reads from its own field name (file1, file2, ...), and you can mix single files and lists of files on the same operation:

from ninja import NinjaAPI, UploadedFile

api = NinjaAPI()


@api.post("/upload-both")
def upload_both(request, file: UploadedFile, extra: list[UploadedFile]):
    return {"file": file.name, "extra": [f.name for f in extra]}

Optional files

Give the parameter a File(None) default (and a | None annotation) to make it optional. A bare None default doesn't work here, since the automatic UploadedFile → File detection only kicks in when the annotation is exactly UploadedFile, not UploadedFile | None:

from ninja import File, NinjaAPI, UploadedFile

api = NinjaAPI()


@api.post("/upload-optional")
def upload_optional(request, file: File[UploadedFile | None] = None):
    if file is None:
        return {"uploaded": False}
    return {"uploaded": True, "name": file.name}

Validation and metadata

Validation and metadata arguments go in P(...), the same as for Form: gt, ge, lt, le, min_length, max_length, pattern, plus alias, title, description, example/examples, deprecated and include_in_schema for the generated OpenAPI schema.

min_length/max_length are most useful on a list of files, where they constrain how many files must be sent:

from ninja import File, NinjaAPI, P, UploadedFile

api = NinjaAPI()


@api.post("/upload-gallery")
def upload_gallery(
    request, images: File[list[UploadedFile], P(min_length=1, max_length=5)]
):
    return [f.name for f in images]

Use alias when the multipart field name isn't a valid Python identifier:

from ninja import File, NinjaAPI, P, UploadedFile

api = NinjaAPI()


@api.post("/upload-legacy")
def upload_legacy(request, file: File[UploadedFile, P(alias="user-file")]):
    return {"name": file.name}

The older default form, file: UploadedFile = File(...), still works and means the same thing as file: File[UploadedFile], but the annotation is preferred.

Files with form data or a JSON body

Because the HTTP protocol has no way to put binary files inside application/json, sending files alongside other data means sending everything as multipart/form-data. Django Ninja does this switch for you automatically whenever an operation has at least one File parameter — you don't need to configure anything.

Mark the extra fields with Form, individually or grouped in a Schema. Each field becomes its own part of the multipart/form-data request:

from datetime import date

from ninja import File, Form, NinjaAPI, Schema, UploadedFile

api = NinjaAPI()


class UserDetails(Schema):
    first_name: str
    last_name: str
    birth_date: date


@api.post("/users")
def create_user(request, details: Form[UserDetails], file: File[UploadedFile]):
    return [details.dict(), file.name]

The client sends first_name, last_name, birth_date and file as four separate multipart/form-data parts.

Leave the schema as a plain (Body-like) parameter instead, and it's sent as a single multipart field whose value is the JSON-encoded object:

from datetime import date

from ninja import File, NinjaAPI, Schema, UploadedFile

api = NinjaAPI()


class UserDetails(Schema):
    first_name: str
    last_name: str
    birth_date: date


@api.post("/users")
def create_user(request, details: UserDetails, file: File[UploadedFile]):
    return [details.dict(), file.name]

The client now sends exactly two multipart/form-data parts: details (the object, serialized to a JSON string) and file.

Both styles combine with list[UploadedFile] the same way — replace file above with images: list[UploadedFile] to accept a Schema plus a gallery of files in one request.

Prefer Form. Form fields are what HTML forms and most HTTP clients send by default, and every field shows up as its own input in the interactive docs. Reach for the JSON body style when the data is nested (lists of objects, nested schemas) and doesn't flatten into form fields well.

When a Body parameter is mixed with File or Form, how its part is read depends on its type:

Parameter type Multipart part is read as
Schema, list, dict, TypedDict, dataclass, Any, … a JSON-encoded string, e.g. {"first_name": "John"}
str, int, date, Enum, Literal, UUID, … plain text, e.g. John — the same as a Form field

The rule follows the parameter's JSON schema: primitives (string, number, integer, boolean) are plain text, anything else is JSON. Optional[...] and Annotated[...] don't change it, and a union that includes a structured type is read as JSON as a whole: for Union[str, dict] send {"a": 1} or "hello" (JSON-quoted).

So for scalar values Body(...) and Form(...) behave the same here; use Form to make the intent clear.

Swagger UI / OpenAPI

This matches the OpenAPI default encoding for multipart/form-data: object properties are sent as application/json, primitive ones as plain text. In Swagger's "Try it out", a JSON body schema appears as a single text area — type the object as JSON there — while scalar fields appear as ordinary inputs. Generated API clients follow the same rules.

Tip

Path and query parameters can still be declared on the same operation — Django Ninja resolves each parameter from its own source regardless of how many File/Form/Body parameters are mixed in.

request.FILES with PUT and PATCH

Django's request.FILES is only populated for POST requests by default — a long standing Django behavior, not specific to Django Ninja. This means a PUT or PATCH operation that declares a File parameter would never actually receive the file, since request.FILES stays empty.

Django Ninja detects this at operation-registration time and raises ninja.errors.ConfigError immediately instead of letting the endpoint fail silently at request time:

@api.patch("/upload")  # raises ConfigError as soon as this module is imported
def upload(request, file: UploadedFile):
    ...

To fix it, add Django Ninja's compatibility middleware to MIDDLEWARE. It manually parses multipart/form-data for the affected methods and populates request.FILES before your view runs:

MIDDLEWARE = [
    # ... your existing middleware ...
    "ninja.compatibility.files.fix_request_files_middleware",
]

By default the middleware (and the ConfigError check) applies to PUT, PATCH and DELETE. Override which methods it covers with the NINJA_FIX_REQUEST_FILES_METHODS setting:

NINJA_FIX_REQUEST_FILES_METHODS = {"PUT", "PATCH"}

Note

The middleware only touches requests it needs to — it leaves normal POST requests, requests with an application/json body, and any method not listed in NINJA_FIX_REQUEST_FILES_METHODS completely alone.

Notes