Skip to content

Request Body and Schema#

2024 iThome Ironman

This is the 12th post in the Django Ninja tutorial series.

After the introduction of the previous posts, we have learned how to handle path and query parameters. However, in the real world, we often need to process more complex request data.

For example, forms submitted by users, uploaded files, and so on. For APIs, the most common is the request body in JSON format.

This article will explore how Django Ninja handles request bodies and introduce how to define and validate data using Schema.

For all code changes in this article, please refer to this PR.

GitHub Example Project#

👉 Django-Ninja-Tutorial


1. What Is a Request Body?#

A request body refers to the data sent along with an HTTP request, usually used for POST, PUT, etc., requests that need to create or update "resources."

This data does not appear in the URL but instead serves as the body of the request in JSON or other formats (such as XML, form-data).

For example, when a user wants to publish a new post, they might send the following request body in JSON format:

{
    "title": "My First Post",
    "content": "This is my first post on Ninja Forum. I hope you enjoy it!"
}

This request body contains the title and content fields, and Django Ninja will help us process and validate this data.


2. Changes in the Example Project#

We need to create an API that receives a request body—"Add Post"—in the example project.

In addition, we need to add a new Python module under the Django post app directory: schemas.py. This is where all Schemas used in the API will be placed.

├── NinjaForum
   ├── ...
├── post
   ├── api.py
   ├── schemas.py # New module added here
   ├── ...

We will introduce the code in the sections that follow.

Starting from this article, branch names will no longer use Chinese, because Chinese branch names keep getting flagged by GitHub:

The head ref may contain hidden characters: ...

And probably very few people use Chinese to name git branches anyway! Using Chinese at the beginning was just to make it easier for readers to follow 🥹

So starting from this branch, they will be changed to number + English, such as "12-request-body" for this post. However, the PR title will still remain in Chinese.


3. Defining and Validating Request Bodies Using Schema#

Like FastAPI, Django Ninja uses Pydantic BaseModel to handle request bodies.

However, because the name BaseModel can easily be confused with Django's Models, Django Ninja renamed it to Schema.

Schema inherits from BaseModel, so the two are essentially the same (with a small Django Ninja-specific addition):

# Django Ninja source code
class Schema(BaseModel, metaclass=ResolverMetaclass):
    ...

Back to the project, let's look at the example in the project, which is the Schema defining the request body for the "Add Post" API:

# post/schemas.py
from ninja import Schema

class CreatePostRequest(Schema):
    title: str
    content: str
    user_id: int

This Schema requires that the body data must contain these three fields: title, content, and user_id, and that the data types must also match.

Using Schema in View Functions#

Once the "request" Schema is defined, you can use it in the view function in the form of a "function parameter":

from post.schemas import CreatePostRequest
...

@router.post(path='/posts/')
def create_post(..., payload: CreatePostRequest):  # Here
    ...

We set the type hint of the function's payload parameter to the CreatePostRequest we just defined.

When a request is sent to this API, Django Ninja uses the CreatePostRequest Schema to parse and validate the data in the body.

After successful validation, the data is passed to the payload of the view function. At this point, the payload parameter inside the function is essentially a Schema (i.e., Pydantic BaseModel) object.

Automatic Data Validation and Error Handling#

If a field is missing in the request body, or the data type is incorrect, Django Ninja will automatically return a 422 response and provide specific error details:

{
    "detail": [
        {
            "type": "missing",
            "loc": [
                "body",
                "payload",
                "content"
            ],
            "msg": "Field required"
        }
    ]
}

The error message indicates: the content field is missing from the body.


4. Optional Fields and Default Values#

In actual API development, not all request fields are mandatory.

We can define optional fields through Pydantic and type hints. Suppose the content of the post is completely optional now (note the content field):

class CreatePostRequest(Schema):
    title: str
    content: str | None = None
    user_id: int

Using the = operator to set the default value of the content field to None makes this field optional. The type hint for content must also be changed to str | None.

It is worth mentioning that if the Schema is used for a request, even if this setting passes validation, you must also pay attention to whether the corresponding Django Model field (i.e., the database field) allows NULL. Otherwise, an error will still occur:

django.db.utils.IntegrityError: NOT NULL constraint failed: post_post.content

In addition to making a field optional, you can also directly provide a default value, such as an empty string here. When the user does not provide input, the default value will be automatically filled:

class CreatePostRequest(Schema):
    title: str
    content: str = ''
    user_id: int

Other than None, however, Default Values in a Schema should be used with great care. We will return to this topic in Article 18, "Pydantic Field Configuration: Examples and Default Values."


5. How Django Ninja Evaluates Parameter Order#

Have you ever wondered, with so many types of view function parameters, how Django Ninja knows which parameter maps to which source?

Indeed, Django Ninja automatically determines the source of parameters (whether it is a path parameter, query parameter, or request body) based on the parameter signature of the view function. Its evaluation order is as follows:

  1. Path Parameters: Any variable defined in the URL path (such as id in /items/{id}) is identified as a path parameter with highest priority.
  2. Query Parameters: Other singular type parameters in the function (such as int, float, bool, str, rather than list, dict), if not annotated as path parameters, will be identified as query parameters.
  3. Request Body: Parameters with Schema types will be treated as the request body.

In principle, a view function can only have one Schema parameter. After all, a request has only one body.


End of Section 2#

The content of this section is almost finished.

In this section, we learned how to handle HTTP requests using Django Ninja and introduced the basic usage of Schema.

Schema has many more uses and variations; this was only a taste. In Section 3, "HTTP Responses," you will see more ways to configure it.

Before entering the next section, let's take a mid-break—and make some preparations.


Mid-Break and Preparation#

In the next section, we want to make the project's API actually work. Do you remember why it couldn't be used earlier?

  1. No database data.
  2. No Schema established.

We have learned how to use Schema—though not fully. The "database data" issue also needs to be resolved.

Django Fixtures#

We could certainly manually add user and post data by calling POST APIs, but that's too troublesome! Not to mention, the project does not yet have a "create user" API.

So, let's not make it complicated.

We will directly import the mock data pre-defined by me via Django fixtures.

For an introduction to Django fixtures, please refer to the article: Importing and Exporting Data with Django Fixture.

In the next article's 13-response branch, you can already see the fixture data I exported:

  1. users.json.
  2. posts.json.

To use them, simply import them sequentially:

python manage.py loaddata users.json
python manage.py loaddata posts.json

You must import users first, otherwise posts will fail to relate without authors.

After importing, you will get two users—Alice and Bob—and 30 posts published by each of them.

Well, my first test post is mixed in there, please bear with me 😅

Once successfully imported, we can proceed.