Skip to content

Resolver and Field Formatting#

2024 iThome Ironman

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

The previous article mentioned that API responses are often filtering and processing of Django Model object contents—followed by JSON serialization.

In more precise terms, that processing is Data Formatting: converting or reorganizing output data according to a set of rules so that it matches a specific format.

There are many types of data formatting, for example:

  1. Time Format Conversion: Converting timestamps in the database to a more readable format.
  2. Numeric Conversion: Converting numbers to currency format or rounding decimal places.
  3. String Processing: Truncating excessively long text, adding a unified prefix, etc.

Regardless of the reason, most of the time it is for the "readability" of the data or to comply with specific business rules.

As you can imagine, needs like data formatting are not only practically important but also very common in API development, deserving a whole article for detailed exploration.

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

GitHub Example Project#

👉 Django-Ninja-Tutorial


Scenarios and Requirements#

Returning again to the "Get Single Post" API, this is the current return format:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "alice@example.com"
    },
    "created_at": "2024-09-12T02:28:16.801Z",
    "updated_at": "2024-09-12T02:28:16.801Z"
}

We decided to simplify the response time string, adopting the format "2024-09-12T02:28:16Z".

Compared with the old version, it only lacks the fractional part .801 and still complies with the ISO 8601 standard.

In short, the contents of the created_at and updated_at fields in the response need to undergo a format conversion. This is the "data formatting" mentioned above.


Django REST Framework Approach#

First, we will inevitably introduce the approach of Django REST framework (hereafter referred to as DRF) to help you compare the differences between the two—you will find that they are actually very similar.

In DRF, we can achieve time format conversion via SerializerMethodField. Here is an example implemented through DRF:

class PostSerializer(serializers.ModelSerializer):
    ...
    created_at = serializers.SerializerMethodField()
    updated_at = serializers.SerializerMethodField()

    def get_created_at(self, obj):
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def get_updated_at(self, obj):
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

There are three key points:

  1. For the field to be formatted, its value must be SerializerMethodField.
  2. In the serializer class, define an instance method (with the first positional parameter self) of the same field name, and prefix the name with get_, such as get_created_at.
  3. The obj parameter refers to the object currently being serialized. In this example, we expect the argument to be a Post model instance. This method will be automatically called during the serialization process, converting the original datetime object into the specified string format.

As a side note, among various instance methods in DRF serializers, the parameter name obj can be considered a naming convention.


Field Data Formatting in Django Ninja#

Having looked at DRF, let's see how Django Ninja does it.

Through Django Ninja's Resolver methods, we can also easily handle such requirements.

Resolver Methods in Django Ninja#

In Django Ninja, we use Resolver methods to achieve the same feature:

class PostResponse(Schema):
    ...
    created_at: datetime
    updated_at: datetime

    @staticmethod
    def resolve_created_at(obj: Post) -> str:
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def resolve_updated_at(self, obj: Post) -> str:
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

The naming of the method, compared to the get_ prefix used in DRF, adopts the resolve_ prefix in Django Ninja.

In addition, you are not mistaken; two writing styles are used here:

  • resolve_created_at is a "static method" that requires the @staticmethod decorator and has no self parameter.
  • resolve_updated_at is a typical instance method with the self parameter.

Because both styles indeed exist in the documentation examples:

class TaskSchema(Schema):
    ...
    owner: Optional[str] = None
    lower_title: str

    @staticmethod
    def resolve_owner(obj):
        if not obj.owner:
            return
        return f"{obj.owner.first_name} {obj.owner.last_name}"

    def resolve_lower_title(self, obj):
        return self.title.lower()

Instance Method Version Not Yet Implemented#

But! At this stage, you only need to know the "static method" version.

Because if you adopt the second style, you will get the following error message:

Error extracting attribute: NotImplementedError: Non static resolves are not supported yet [type=get_attribute_error, input_value=>, input_type=DjangoGetter]

What? Not implemented yet!

I had to obediently change them all to static methods.

Result#

Finally, let's look at the new response of the "Get Single Post" API:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "alice@example.com"
    },
    "created_at": "2024-09-12T02:28:16Z",
    "updated_at": "2024-09-12T02:28:16Z"
}

Great, the time string format has been successfully converted—it is 16Z instead of 16.801Z.


Using Alias to Flatten Field Information#

Another common formatting requirement is the "flattening" of complex data structures we mentioned earlier.

This is a "reorganization" of data, and structural reorganization also belongs to the category of data formatting explored in this article.

Remember in Post 14, how we generated the author_name field content in the "Get Post List" response via @property? — That was a flattening of the User model to directly retrieve its username field details.

Here, let's switch to a more elegant approach—alias.

Using Alias#

Django Ninja (almost directly copied from Pydantic) provides the Field and alias parameters to implement this feature.

We will cover Field in greater depth in Article 18, "Pydantic Field Configuration: Examples and Default Values."

Let's first see how to use it:

class PostListResponse(Schema):
    id: int
    title: str
    created_at: datetime
    author_name: str = Field(alias='author.username')

Note that the original @property method on the Post model must be removed, or at least it cannot have the same name as author_name, otherwise an error will occur!

I chose to remove the @property method and directly switch to this new approach.

Key Points#

The alias='author.username' setting retrieves username from the Related Model, the User associated with the Post. This flattens the Nested Structure.

This design is obviously an excellent borrow from DRF, equivalent to the source=author.username syntax in DRF.

Although it's a bit abstract, it is extremely elegant.

The use of alias is not limited to data flattening (which is actually a more advanced usage). For other details like field name replacement, you can directly refer to the Pydantic Documentation.

Result#

The effect of this approach is exactly the same as using @property before:

// http://127.0.0.1:8000/posts/
[
    {
        "id": 1,
        "title": "Alice's Django Ninja Post 1",
        "created_at": "2024-09-12T02:28:16.801Z",
        "author_name": "Alice"  // Flattened author username
    }
]

As you can see, the author_name field has been successfully flattened, directly displaying the author's name.


Conclusion#

Django Ninja's Resolver methods allow us to dynamically process field data in API responses, meeting various format conversion and custom requirements.

When handling time fields like created_at and updated_at, Resolver methods are not only simple and easy to use but also guarantee that the code structure remains clear.

The Field and alias parameters offer a more elegant implementation of another common data formatting—"flattening". It not only simplifies API responses but also avoids modifying the underlying Django model.

Together, these techniques give us more flexible control over API output so that we can meet client needs.

Preview of the Next Chapter#

Having completed the learning of the 4 posts on "Django Ninja Handling HTTP Responses", Chapter 3 has officially come to an end. Next, we will turn our attention to another important topic in API development—documentation!

As the project size grows, clear API documentation is crucial for anyone who needs to use the API—including backend developers themselves!

Good API Documentation can greatly reduce communication costs, improve development efficiency, and prevent errors. It is both technical documentation and an important hub for team collaboration.

Chapter 4, we will explore how to effectively generate high-quality API documentation through Django Ninja code, thereby enhancing the overall development experience.