In Django Ninja, how does an operation decide whether a parameter comes from the path, the query string or the request body?
answer
- the signature is read at registration
- explicit markers beat inference
- names that appear in the URL
- models and lists go elsewhere
basics
~20 sDjango Ninja applies rules in order: an explicit Query/Path/Body/Form/File/Header/Cookie marker wins; a name in the path is a path parameter; a list, set, tuple or Schema is body; any other scalar is a query parameter.
solid answer
~40 sNinja inspects the signature when the decorator runs and assigns each parameter a source. First, an explicit marker decides: `Query(...)`, `Path(...)`, `Body(...)`, `Form(...)`, `File(...)`, `Header(...)`, `Cookie(...)`, or the `Annotated` form like `Query[list[str]]`. Otherwise a name that appears in the path, like `{warehouse_id}`, is a path parameter and may not have a default. A `list`, `set`, `tuple` or Pydantic model such as a `Schema` is read from the body. Anything else is a query parameter, optional if it has a default. The classic traps: `skus: list[str]` is body, not repeated query values, and a filter schema on a GET is still body unless written `Query[Filters]`. One body parameter is the whole JSON body; two are keyed by parameter name.
code
python · 25 linesfrom ninja import Query, Router, Schema
router = Router()
class StockIn(Schema):
sku: str
quantity: int
class StockFilters(Schema):
warehouse: str | None = None
low_stock: bool = False
@router.post("/warehouses/{warehouse_id}/stock")
def add_stock(request, warehouse_id: int, payload: StockIn, dry_run: bool = False):
# warehouse_id: path, payload: body, dry_run: query
return {"warehouse": warehouse_id, "sku": payload.sku, "dry_run": dry_run}
@router.get("/stock")
def list_stock(request, filters: Query[StockFilters], skus: list[str] = Query(None)):
# GET /stock?warehouse=north&low_stock=true&skus=A-1&skus=B-2
return {"filters": filters.dict(), "skus": skus}go deeper
Recall the basic split: names in the path come from the URL, Schema parameters from the body, and plain scalars from the query string.
Explain the full order of rules, including explicit markers, the collection-to-body rule, Query[Schema] flattening and the single versus multiple body parameter shapes.
Diagnose unexpected 422s from their error locations, and write signatures whose sources are explicit wherever inference would surprise a reader.
Set a team convention on when explicit markers are mandatory, so signatures stay readable and the generated schema matches what clients send.
## Ninja reads the function signature once When an operation is registered — at import time, when the decorator runs — Django Ninja inspects the function's signature and builds validation models for each **parameter source**: path, query, header, cookie, form, file and body. At request time it pulls each value from its source, validates it with Pydantic, and calls the function with typed arguments. Parameters named `request` and `**kwargs` are skipped. ## The inference rules, in order For each parameter, the first rule that matches decides the source: 1. **An explicit marker wins.** If the default is one of Ninja's parameter markers — `Query(...)`, `Path(...)`, `Body(...)`, `Form(...)`, `File(...)`, `Header(...)`, `Cookie(...)` — or the annotation uses the `Annotated` shortcut such as `Query[list[str]]`, that is the source. 2. **A name in the path is a path parameter.** If the name appears as `{name}` or `{converter:name}` in the operation's path, it is read from the URL. A default value is not allowed here: registration fails with an assertion. 3. **Collections and schemas are body.** A `list`, `set` or `tuple` annotation, or a Pydantic model such as a Ninja `Schema`, is read from the request **body**. 4. **Everything else is query.** Scalars (`int`, `str`, `bool`, `date`, …) come from the **query string**; required when there is no default, optional when there is one. Two smaller rules: an **unannotated** parameter is typed from its default (or as `str` with no default), and an `UploadedFile` annotation is treated as a file even without `File(...)`. ## Worked example from an inventory API | Parameter | Declared as | Source | Why | |---|---|---|---| | `warehouse_id` | `warehouse_id: int` with path `/warehouses/{warehouse_id}/stock` | Path | Name is in the path | | `payload` | `payload: StockIn` (a `Schema`) | Body | Pydantic model | | `dry_run` | `dry_run: bool = False` | Query, optional | Scalar with default | | `skus` | `skus: list[str]` | **Body** | Collections default to body | | `skus` | `skus: list[str] = Query(None)` | Query, repeated `?skus=` | Explicit marker | | `filters` | `filters: Query[StockFilters]` | Query, fields flattened | Explicit marker on a schema | ## The surprises interviewers probe - **`list[str]` without a marker is body, not query.** A `GET /stock?skus=A&skus=B` endpoint written as `skus: list[str]` expects a JSON body; the fix is `Query(...)`. - **A schema on a GET is still body.** The HTTP method never changes inference; to read a filter schema from the query string, use `Query[StockFilters]`, and its fields appear as individual query parameters. - **One body parameter is the whole body; two are keyed.** With a single `payload: StockIn`, the JSON body *is* the `StockIn` object. With `payload: StockIn` and `note: NoteIn`, the body must be `{"payload": {...}, "note": {...}}`. - **Path names must match.** A path placeholder with no matching parameter triggers a warning that the field is in the view path but not in the signature; the operation then cannot receive it. - **`x=Schema` instead of `x: Schema`** — a schema class used as a default rather than an annotation — raises a `ConfigError` telling you to use the annotation. - **Flattened names must be unique.** Two query schemas that both define `warehouse` raise a `ConfigError` about a duplicated name. ## Headers, cookies, forms and files The same machinery covers the other sources, but through explicit markers — an `UploadedFile` annotation is the one automatic case: `Header(...)` and `Cookie(...)` read request headers and cookies, `Form(...)` reads form-encoded fields (and `Form[SomeSchema]` a whole schema from a form), and `File(...)` reads uploads. When an operation mixes `File` or `Form` parameters with body parameters, Ninja builds a single multipart body model for them, so the body parameters are read from the same multipart request as the files. None of these sources is ever inferred from a parameter's name. ## What happens on bad input Validation failures from any source — a non-integer `warehouse_id`, a missing required query parameter, a body field of the wrong type — are collected and returned as a `422` response whose `detail` lists each error with its location, such as `["query", "dry_run"]` or `["body", "payload", "quantity"]`. A body that is not parseable at all is rejected earlier with `400` ("Cannot parse request body"). The error format itself is owned by Ninja's schemas and errors topic; here the point is that the location names the source the inference chose.
- Why does skus: list[str] expect a JSON body rather than ?skus=A&skus=B?Ninja's inference treats any collection annotation (`list`, `set`, `tuple`) like a Pydantic model and assigns it to the body. Only an explicit marker changes that: `skus: list[str] = Query(None)` or `skus: Query[list[str]]` reads repeated query values, which Ninja collects from Django's `QueryDict` as a list.
- What does the JSON body look like when an operation takes two schema parameters?With one body parameter, Ninja reads the whole body as that schema. With two or more, it expects an object keyed by parameter name, for example `{"payload": {"sku": "A-1", "quantity": 3}, "note": {"text": "restock"}}`. Wrapping a single schema in its parameter name is therefore a common client bug that produces a 422.
- Can a path parameter have a default value in Django Ninja?No. If the parameter's name appears in the operation path, Ninja asserts at registration that it has no default, so the module fails to import. Make the segment its own route, or turn the value into an optional query parameter instead.
saying these in an interview costs you the question
- Believing list[str] parameters are read from repeated query values by default
- Thinking the HTTP method decides whether a schema comes from query or body
- Assuming a default value makes a parameter a body field
- Wrapping a single schema's JSON inside its parameter name
- Giving a path parameter a default to make the URL segment optional