Skip to content

Form Data

To read application/x-www-form-urlencoded or multipart/form-data data — the way an HTML <form> submits — annotate a parameter with Form, the same way you'd use Query or Path. Django Ninja reads it from request.POST, parses it according to the type hint and validates it, same as any other parameter source.

Basic usage

from ninja import NinjaAPI, Form

api = NinjaAPI()


@api.post("/login")
def login(request, username: Form[str], password: Form[str]):
    return {"username": username, "password": "*****"}

Form[str] is shorthand for username: str = Form(...) — use whichever reads better:

@api.post("/login")
def login(request, username: str = Form(...), password: str = Form(...)):
    return {"username": username, "password": "*****"}

A default value makes the field optional:

@api.post("/login")
def login(request, remember_me: bool = Form(False)):
    return {"remember_me": remember_me}

Grouping parameters into a Schema

In the same way as Query or Body, group related form fields into a Schema and annotate it with Form:

from ninja import Form, NinjaAPI, Schema

api = NinjaAPI()


class Item(Schema):
    name: str
    description: str | None = None
    price: float
    quantity: int


@api.post("/items")
def create(request, item: Form[Item]):
    return item

Every field of Item is read from the request's form fields, using each field's own type, default and validation.

Combining with path and query parameters

You can declare form fields alongside path and query parameters on the same operation — Django Ninja resolves each function parameter from its own source (path, query, form, ...) and calls your view with all of them:

from ninja import Form, NinjaAPI, Schema

api = NinjaAPI()


class Item(Schema):
    name: str
    description: str | None = None
    price: float
    quantity: int


@api.post("/items/{item_id}")
def update(request, item_id: int, q: str, item: Form[Item]):
    return {"item_id": item_id, "item": item.dict(), "q": q}

Here item_id is taken from the path, q from the query string, and item from the form fields — all in the same call.

Validation constraints

Form() accepts the same validation arguments as Query: gt, ge, lt, le for numbers, min_length, max_length, pattern for strings, plus title, description, example/examples, deprecated and include_in_schema for the generated OpenAPI schema.

@api.post("/items/quick")
def create_quick(
    request,
    name: str = Form(..., min_length=1, max_length=100),
    quantity: int = Form(1, gt=0),
):
    return {"name": name, "quantity": quantity}

The same constraints can be set with a plain Pydantic Field when the parameter is a field of a Form schema.

Aliases

Use alias when the form field name isn't a valid Python identifier, or simply differs from your parameter name:

@api.post("/items/legacy")
def create_legacy(request, name: str = Form(..., alias="item-name")):
    return {"name": name}

Inside a Schema, set the alias with Pydantic's Field(alias=...) instead.

Mapping empty form fields to a default

HTML forms often submit optional fields as an empty string rather than omitting them, which fails validation for a type such as int, float or bool. Fix this with a wrap validator that falls back to the field's default whenever the incoming value is an empty string — see the Pydantic docs on wrap validators:

from typing import Annotated, TypeVar

from pydantic import WrapValidator
from pydantic_core import PydanticUseDefault

from ninja import Form, NinjaAPI, Schema

api = NinjaAPI()

T = TypeVar("T")


def _empty_str_to_default(v, handler, info):
    if isinstance(v, str) and v == "":
        raise PydanticUseDefault
    return handler(v)


EmptyStrToDefault = Annotated[T, WrapValidator(_empty_str_to_default)]


class Item(Schema):
    name: str
    description: str | None = None
    price: EmptyStrToDefault[float] = 0.0
    quantity: EmptyStrToDefault[int] = 0
    in_stock: EmptyStrToDefault[bool] = True


@api.post("/items-blank-default")
def create_with_defaults(request, item: Form[Item]):
    return item.dict()

Posting price="" and quantity="" now falls back to 0.0 and 0 instead of failing validation.

Combining with file uploads

If an operation declares Form fields (or plain Body fields) together with one or more File parameters, Django Ninja automatically treats the whole request as multipart/form-data — you don't need to do anything differently:

from ninja import File, Form, NinjaAPI, UploadedFile

api = NinjaAPI()


@api.post("/upload")
def upload(request, title: str = Form(...), file: UploadedFile = File(...)):
    return {"title": title, "size": file.size}

See File Uploads for everything about File, including multiple files and validation.

Tip

Form parameters that aren't declared on the operation are ignored — you don't need to enumerate every field the client might send, only the ones you read.