Skip to content

Request Body

Request bodies are typically used with “create” and “update” operations (POST, PUT, PATCH). For example, when creating a resource using POST or PUT, the request body usually contains the representation of the resource to be created.

To declare a request body, you need to use Django Ninja Schema.

Info

Under the hood Django Ninja uses Pydantic models with all their power and benefits. The alias Schema was chosen to avoid confusion in code when using Django models, as Pydantic's model class is called Model by default, and conflicts with Django's Model class.

Import Schema

First, you need to import Schema from hattori:

from typing import Annotated

from hattori import Response, Schema


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


@api.post("/items", url_name="create_item")
def create(request, item: Item) -> Annotated[Response[Item], 200]:
    return Response(200, item)

Create your data model

Then you declare your data model as a class that inherits from Schema.

Use standard Python types for all the attributes:

from typing import Annotated

from hattori import Response, Schema


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


@api.post("/items", url_name="create_item")
def create(request, item: Item) -> Annotated[Response[Item], 200]:
    return Response(200, item)

Note: if you use None as the default value for an attribute, it will become optional in the request body. For example, this model above declares a JSON "object" (or Python dict) like:

{
    "name": "Katana",
    "description": "An optional description",
    "price": 299.00,
    "quantity": 10
}

...as description is optional (with a default value of None), this JSON "object" would also be valid:

{
    "name": "Katana",
    "price": 299.00,
    "quantity": 10
}

Declare it as a parameter

To add it to your path operation, declare it the same way you declared the path and query parameters:

from typing import Annotated

from hattori import Response, Schema


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


@api.post("/items", url_name="create_item")
def create(request, item: Item) -> Annotated[Response[Item], 200]:
    return Response(200, item)

... and declare its type as the model you created, Item.

Results

With just that Python type declaration, Django Ninja will:

  • Read the body of the request as JSON.
  • Convert the corresponding types (if needed).
  • Validate the data.
    • If the data is invalid, it will return a nice and meaningful error, indicating exactly where and what the incorrect data was.
  • Give you the received data in the parameter item.
    • Because you declared it in the function to be of type Item, you will also have all the editor support (completion, etc.) for all the attributes and their types.
  • Generate JSON Schema definitions for your models, and you can also use them anywhere else you like if it makes sense for your project.
  • Those schemas will be part of the generated OpenAPI schema, and used by the automatic documentation UI's.

Automatic docs

The JSON Schemas of your models will be part of your OpenAPI generated schema, and will be shown in the interactive API docs:

Openapi schema

... and they will be also used in the API docs inside each path operation that needs them:

Openapi schema

Editor support

In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if you received a dict instead of a Schema object):

Type hints

The previous screenshots were taken with Visual Studio Code.

You would get the same editor support with PyCharm and most of the other Python editors.

Request body + path parameters

You can declare path parameters and body requests at the same time.

Django Ninja will recognize that the function parameters that match path parameters should be taken from the path, and that function parameters that are declared with Schema should be taken from the request body.

from typing import Annotated

from hattori import Response, Schema


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


class ItemUpdate(Schema):
    item_id: int
    item: Item


@api.put("/items/{item_id}", url_name="update_item_put")
def update(request, item_id: int, item: Item) -> Annotated[Response[ItemUpdate], 200]:
    return Response(200, {"item_id": item_id, "item": item})

Request body + path + query parameters

You can also declare body, path and query parameters, all at the same time.

Django Ninja will recognize each of them and take the data from the correct place.

from typing import Annotated

from hattori import Response, Schema


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


class ItemUpdateResponse(Schema):
    item_id: int
    item: Item
    q: str


@api.post("/items/{item_id}", url_name="update_item_post")
def update(request, item_id: int, item: Item, q: str) -> Annotated[Response[ItemUpdateResponse], 200]:
    return Response(200, {"item_id": item_id, "item": item, "q": q})

The function parameters will be recognized as follows:

  • If the parameter is also declared in the path, it will be used as a path parameter.
  • If the parameter is of a singular type (like int, float, str, bool, etc.), it will be interpreted as a query parameter.
  • If the parameter is declared to be of the type of Schema (or Pydantic BaseModel), it will be interpreted as a request body.

Partial updates with PatchDict

When handling PATCH requests, you typically want to update only the fields the client actually sent — and ignore the rest. The challenge is distinguishing between "field was omitted" (don't touch it) and "field was set to null" (clear it).

Django Ninja provides PatchDict to solve this. It takes your schema, makes all fields optional, and returns a dict containing only the fields that were present in the request body:

from typing import Annotated

from hattori import PatchDict, Response, Schema


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


@api.patch("/items/{item_id}")
def update_item(
    request, item_id: int, payload: PatchDict[ItemUpdate]
) -> Annotated[Response[ItemUpdate], 200]:
    # payload is a dict containing only the fields the client sent
    # e.g. {"price": 9.99} — other fields are excluded
    item = get_item(item_id)
    for key, value in payload.items():
        setattr(item, key, value)
    item.save()
    return Response(200, item)

If the client sends {"price": 9.99}, payload will be {"price": 9.99} — the other fields won't appear in the dict at all.

This works by combining two Pydantic features under the hood:

  • All fields are made optional so partial payloads pass validation.
  • exclude_unset=True is applied so only explicitly sent fields are included in the result.