コンテンツにスキップ

Resolverとフィールドのフォーマット化#

2024 iThome鉄人レース

これはDjango Ninjaシリーズチュートリアルの第16回です。

前回の記事で、APIのレスポンスはしばしばDjangoモデルオブジェクトの内容に対するフィルタリングと加工——そしてJSONのシリアライズであると述べました。

その中の「加工」の部分は、より専門的な言葉を使えば「データのフォーマット化」——特定のルールに従って、出力データを特定の出力形式に合致するように何らかの変換や再構成を行うことだと言えるでしょう。

データのフォーマット化の種類は多く、例えば以下のようなものがあります:

  1. 時間形式の変換:データベース内のタイムスタンプ(timestamp)を、より読みやすい形式に変換する。
  2. 数値の変換:数値を通貨形式に変換したり、小数点以下の桁数を四捨五入したりする。
  3. 文字列処理:長すぎるテキストを切り詰めたり、統一されたプレフィックスを追加したりする。

理由が何であれ、その大部分はデータの「可読性」のため、あるいは特定のビジネスルールに適合させるためのものです。

ご想像の通り、データのフォーマット化のような要件は、実務的に重要であるだけでなく、API開発においても非常に一般的であり、一つの記事全体を割いて詳しく探求する価値があります。

この記事のすべてのコードの変更は、こちらのPRを参考にしてください。

GitHubサンプルプロジェクト#

👉 Django-Ninja-Tutorial


背景と要件#

再び「単一記事情報の取得」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:16.801Z",
    "updated_at": "2024-09-12T02:28:16.801Z"
}

私たちはレスポンスの時間文字列を簡略化し、「"2024-09-12T02:28:16Z"」という形式に変更することにしました。

旧バージョンと比較すると、「.801」という小数部分がなくなっただけで、引き続きISO 8601標準に準拠しています。

要するに、レスポンス内のcreated_atupdated_atの2つのフィールドの内容について、形式上の変換を行う必要があります。すなわち、前述の「データのフォーマット化」です。


Django REST frameworkのアプローチ#

まず、恒例としてDjango REST framework(以下DRFと略します)のアプローチを紹介し、両者の違いを比較しやすくします——実はほとんど同じであることが分かるでしょう。

DRFでは、SerializerMethodFieldを通じて時間形式の変換を実現できます。以下は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')

この中の重要なポイントは3つです:

  1. フォーマット化するフィールドの値は、SerializerMethodFieldである必要があります。
  2. シリアライザクラス内に、同じフィールド名のインスタンスメソッド(第一位置引数selfを持つ)を定義し、命名時にget_のプレフィックス(例えばget_created_at)を追加します。
  3. objパラメータは、現在シリアライズされているオブジェクトを指します。この例では、引数がPostモデルのインスタンスであることを想定しています。このメソッドはシリアライズプロセス中に自動的に呼び出され、元のdatetimeオブジェクトを指定された文字列形式に変換します。

ちなみに、DRFシリアライザの各種インスタンスメソッドにおいて、objというパラメータ名は命名の慣例と呼べるものです。


Django Ninjaのフィールドデータのフォーマット化#

DRFを見たところで、Django Ninjaではどうするかを見てみましょう。

Django NinjaのResolverメソッドを通じて、私たちもこのような要件を簡単に処理できます。

Django NinjaのResolverメソッド#

Django Ninjaでは、Resolverメソッドを用いて同じ機能を実現します:

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')

メソッドの命名について、DRFがget_プレフィックスを使用するのに対し、Django Ninjaはresolve_プレフィックスを採用しています。

また、見間違いではありません。ここでは2つの書き方を使用しています:

  • resolve_created_atは「静的メソッド(static method)」であり、@staticmethodデコレータを必要とし、selfパラメータを持ちません。
  • resolve_updated_atは典型的なインスタンスメソッドであり、selfパラメータを持ちます。

なぜなら、ドキュメントの例に実際にこれら2つの書き方が存在するからです:

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()

インスタンスメソッドバージョンは未実装#

しかし!現段階では、「静的メソッド」のバージョンだけを知っていれば十分です。

なぜなら、2つ目の書き方を採用すると、次のようなエラーメッセージが表示されるからです:

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

なんだって?まだ実装されていないだと!

仕方なく、すべて静的メソッドに変更するしかありませんでした。

レスポンス#

最後に効果のほどを見てみましょう:

// 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"
}

素晴らしい、時間文字列の形式が正常に変換されています——16.801Zではなく16Zになっています。


Aliasを使用したフィールド情報のフラット化#

もう一つの一般的なフォーマット化の要件は、以前に言及した複雑なデータ構造の「フラット化」(flatten)です。

これはデータに対する一種の「再構成」であり、構造の再構成も同様にこの記事で探求するデータのフォーマット化の範疇に属します。

第14回で、@propertyを通じて「記事リストの取得」レスポンス内のauthor_nameフィールドの内容を生成したことを覚えていますか?——これはUserモデルのフラット化であり、そのusernameフィールドの情報を直接取得するものでした。

ここでは、よりエレガントなアプローチであるaliasに切り替えましょう。

Aliasの使用#

Django Ninjaは(ほとんどPydanticからそのまま持ち込んだものですが)、この機能を実現するためにFieldaliasパラメータを提供しています。

Fieldについては、「第18回:Pydantic Fieldの設定例とデフォルト値」でさらに詳しく取り上げます。

まずは使い方を見てみましょう:

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

注意として、元のPostモデルの@propertyメソッドを取り除くか、少なくともauthor_name名前が衝突しないようにしなければなりません。そうしないとエラーになりますよ!

私は@propertyメソッドを削除し、直接この新しいアプローチに切り替えることを選びました。

ポイント解説#

alias=author.usernameを通じて、Post関連モデルであるUserusername属性値を取得します。これにより、ネストされたデータのフラット化が実現しました。

この設計は、明らかにDRFの優れた点を参考にしたものであり、DRFにおけるsource=author.usernameという書き方に相当します。

少し抽象的ではありますが、非常にエレガントです。

aliasの用途はデータのフラット化に限定されず(これはむしろ高度な使い方です)、フィールド名の置換など、その他の詳細についてはPydanticドキュメントを直接参照してください。

レスポンス#

この方法は、以前に@propertyを使用した時と効果は完全に同じです:

// 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"  // フラット化後の作者名
    }
]
author_nameフィールドが正常にフラット化され、作者の名前が直接表示されていることがわかります。


結語#

Django NinjaのResolverメソッドを使用すると、APIレスポンス内のフィールドデータを動的に処理でき、さまざまなフォーマット変換やカスタマイズの要件を満たすことができます。

created_atupdated_atのような時間フィールドを処理する際、Resolverメソッドはシンプルで使いやすいだけでなく、コードの構造を明確に保つことができます。

一方、Fieldaliasパラメータは、もう一つの一般的なデータのフォーマット化である「フラット化」をよりエレガントに実現します。APIレスポンスを簡略化するだけでなく、背後にあるDjangoモデルを変更する必要もありません。

これらの方法を通じて、クライアントの要件に合わせてAPIの出力をより柔軟に制御できるようになります。

次章の予告#

Django NinjaがHTTPレスポンスを処理する」に関する全4回の学習が完了し、第3章も正式に一段落しました。次は、API開発におけるもう一つの重要なテーマ——ドキュメントに目を向けましょう!

プロジェクトの規模が大きくなるにつれ、APIを使用する必要があるすべての人(バックエンド開発者自身も含まれます!)にとって、明確なAPIドキュメント極めて重要になります。

優れたAPIドキュメントは、コミュニケーションコストを大幅に削減し、開発効率を向上させ、エラーを減らすことができます。それは技術文書であるだけでなく、チームコラボレーションの重要なハブでもあります。

第4章では、Django Ninjaのコードを通じて高品質なAPIドキュメントを効果的に生成し、それによって全体的な開発体験を向上させる方法を探求します。