Skip to content

API Documentation (Part 2)#


2024 iThome Ironman Challenge

This is the 18th article in the Django Ninja series.

In the previous article, we explored some important settings that affect the presentation of the API documentation in Django Ninja. These are the basic skills of automated API documentation and should not be ignored.

But that's not enough! We want to make this documentation more vivid, clear, and easy to understand.

The key lies in the data examples in the API documentation. Good examples are immediately understandable, effectively shortening comprehension and thinking time.

This article will introduce how to use Pydantic's Field settings to comprehensively improve the clarity and readability of API documentation. We will explore how to add lifelike examples to automatically generated documentation, making it closer to reality.

All code changes in this article can be referred to in this PR.

GitHub Example Project#

👉 Django-Ninja-Tutorial


The Role of Pydantic in Django Ninja#

Pydantic is a package that achieves data validation and serialization, widely used in frameworks like FastAPI and Django Ninja.

In Django Ninja, Pydantic is used to define Schemas. These Schemas determine how the API processes data in HTTP requests and responses, and automatically convert them into OpenAPI-compliant documentation.

The power of Pydantic lies in the fact that it not only validates data but can also provide extra descriptions, examples, and default values for document fields through Field settings.

These settings are automatically reflected in the generated API Documentation, helping developers understand the API's behavior and data more clearly.

Pydantic Field#

Pydantic's Field is a powerful tool that can be used to provide more detailed information for each data field, such as titles, descriptions, examples, and default values.

These settings not only assist in data validation but also significantly improve the readability of API documentation. Here are some common Field parameters:

  • title: Sets a title for the field to help developers quickly understand its role.
  • description: Provides a description of the field, making its purpose and constraints clearer.
  • examples: Sets example values to help developers intuitively understand the API's input and output formats.
  • default: The first positional parameter, which provides a default value for the field. When the input does not provide a value for this field, the default value will be automatically used.

Making good use of these parameters can produce high-quality API documentation.

Balancing Code and Documentation#

However, we still need to be practical. Writing this much content for every API could become an overwhelming burden for developers.

Moreover, while using a lot of parameters indeed makes the documentation look better, the code generating the documentation will inevitably become quite lengthy!

We need to find a balance point that provides enough information without making the code excessively verbose.

From this perspective, I believe the two most important parameters are default and examples—especially the latter!

Therefore, this article will focus on introducing these two, which not only sharpens the learning focus but also aligns with my daily development routine.


Official Documentation and Source Code#

If you want to learn more about the parameters and usage of Pydantic Field, you should look at Pydantic's Official Documentation—rather than Django Ninja's.

In Django Ninja's documentation, there is no dedicated chapter introducing the use of Field. This is because Field is actually a feature of Pydantic, not specific to Django Ninja.

However, if you actually read that documentation, you might find that its explanations of all Field parameters are not particularly exhaustive.

If you want to know all available parameters, I think looking at the Source Code is the fastest way. Extrapolating its usage from the type hints of the function signature (yes, Field is a function) is also a good approach.


Below, we begin to explain how to use the examples and default parameters of Pydantic Field to make the API documentation more vivid and rigorous.

Adding "Examples" to API Documentation#

In the previous article, we mentioned the deficiencies of the current API documentation, where the issue of "lacking real-world examples" remains unresolved.

Below is an example response in the documentation for "Get Single Post Info":

{
    "id": 0,
    "title": "string",
    "content": "string",
    "author": {
        "id": 0,
        "username": "string",
        "email": "string"
    },
    "created_at": "2024-09-22T08:58:55.960Z",
    "updated_at": "2024-09-22T08:58:55.960Z"
}

Neither 0 nor "string" serves as a good documentation example—they are not realistic enough.

Now, we will add examples to the response Schema. The code is as follows:

class _AuthorInfo(Schema):
    id: int = Field(examples=[1])
    username: str = Field(examples=['Alice'])
    email: str = Field(examples=['alice@example.com'])

class PostResponse(Schema):
    id: int = Field(examples=[1])
    title: str = Field(examples=['Ninja is awesome!'])
    content: str = Field(examples=['This is my first post.'])
    author: _AuthorInfo
    created_at: datetime = Field(examples=['2021-01-01T00:00:00Z'])
    updated_at: datetime = Field(examples=['2021-01-01T00:00:00Z'])
    ...

There are two key points here.

Key Point 1: The examples Parameter#

I have only used the examples parameter. This is the simplest way, and examples are indeed a highly important part of documentation.

Furthermore, there is more to this examples parameter than meets the eye. If you write it as example, such as:

class PostResponse(Schema):
    id: int = Field(example=1)

It actually works fine too, but Mypy will warn you:

Unexpected keyword argument "example" for "Field"; did you mean "examples"?

That's right, because in the current Pydantic v2, Field only has the examples parameter. example was likely the Pydantic v1 approach, and Django Ninja maintains compatibility with both.

Considering the future, it is recommended to use examples, which not only avoids Mypy warnings but also aligns with the latest version of Pydantic.

Key Point 2: Nested Schema Examples#

For nested Schema examples, you only need to add Field to the lowest level Schemas. The referencing layer does not need declarations:

class _AuthorInfo(Schema):  # This is the nested base; Field must be written
    id: int = Field(examples=[1])
    username: str = Field(examples=['Alice'])
    email: str = Field(examples=['alice@example.com'])

class PostResponse(Schema):
    ...
    author: _AuthorInfo  # No need to write Field again
    ...

Result#

Let's look at the resulting API Documentation example. I will extract the JSON directly from the page:

{
    "id": 1,
    "title": "Ninja is awesome!",
    "content": "This is my first post.",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "alice@example.com"
    },
    "created_at": "2021-01-01T00:00:00Z",
    "updated_at": "2021-01-01T00:00:00Z"
}

Compared to the previous 0 and "string", is it much more vivid and readable?


Proper Use Cases for the default Parameter#

In my opinion, most of the time, we do not need to define default values.

I recommend that you do not write it like this:

class PostResponse(Schema):
    id: int = 1
    ...

Although the documentation will similarly display the example value as 1, this approach is actually equivalent to the following:

class PostResponse(Schema):
    id: int = Field(default=1)
    ...

This is actually defining a default value. As mentioned earlier, if the Schema is used in an HTTP request and the client does not provide a value for that field, Django Ninja will automatically use the default value.

This can easily lead to unexpected results.

The correct workflow is: when the frontend does not provide a value, Django Ninja should return a 422 response.

Therefore, you do not need (and should not) define default values—except in the following situation.

Using None as the Default Value for Optional Fields#

I personally recommend using the default parameter only when the request field is "optional."

Furthermore, the default value in this case should be None.

To demonstrate this, let's create a new API—"Create User" (which is user registration). This API will be repeatedly mentioned and improved in subsequent tutorials.

Do you remember that in our User model, the bio field is optional?

class User(AbstractUser):
    email = models.EmailField(unique=True)  # Enforces unique email
    bio = models.TextField(null=True)  # Biography field (optional)
    ...

Therefore, our API request Schema is as follows—look directly at the bio field configuration:

class CreateUserRequest(Schema):
    ...
    bio: str | None = Field(
        default=None,
        examples=['Hello, I am Alice.']
    )

The default=None setting in the Field ensures that the API will not raise an error even if the client does not fill in a value.

Also, pay attention to the bio: str | None type hint. Do not omit None, as it will affect the rendering of the documentation: (this is the result with None)

With None, the API documentation will show the field value as optional (string | null).


Summary and Next Steps#

Through the learning and improvements in this chapter, our API documentation has reached an 80-point level! In most development projects, this level of documentation quality can be considered outstanding.

Next, we will enter Chapter 5Data Validation and Error Handling.

This chapter will cover how to implement effective data validation in Django Ninja, as well as how to gracefully handle and respond to various potential error situations.

With these techniques, we will be able to build more robust and reliable APIs.