コンテンツにスキップ

なぜModelSchemaを使わないのか?#

2024 iThome鉄人レース

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

Django APIのレスポンスは、しばしばModelオブジェクト(すなわちdbデータ)の内容に対する一定のフィルタリングと加工となります。

例えば「単一記事情報の取得」APIは、実際にはPostオブジェクトからフィールドを選び出し、それをシリアライズするものです。

この過程で、私たちはモデルオブジェクトをAPIのレスポンス構造に変換する方法を考慮すると同時に、コードの保守性と柔軟性を維持する必要があります。

これに対して、Django REST framework(以下DRFと略します)は非常に実用的な「特製」シリアライザであるModelSerializerを提供しており、これはDRF開発者が必ず学ぶべき中核的な機能と言えます。

Django Ninjaにも似たような実装であるModelSchemaがありますが、私にとっては使い勝手の悪い存在であり、ほとんど使用したことがありません

このような違いは、間違いなく両者の中核的な設計理念の違いによるものです。

私たちは第3回で、両者の機能面での主な違いについて議論しました。この記事では、「Djangoモデルオブジェクトのシリアライズ」という非常に代表的なトピックを通じて、「なぜDRFに比べて、私がDjango Ninjaを書くことをより好むのか」を説明します。

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

👉 Django-Ninja-Tutorial


ModelSerializerの特徴#

DRFにおけるModelSerializerは非常に強力なツールであり、Djangoモデルを自動的にAPIが必要とするデータ構造——シリアライザに変換し、「シリアライザのフィールドを定義する」プロセスを大幅に簡略化します。

ちなみに、DRFのシリアライザは、Django Ninjaが使用するスキーマに相当し、両者の概念はほぼ同じで、いずれもデータの検証とシリアライズに使用されます。

もし「単一記事情報の取得」APIのレスポンスをModelSerializerで書き換えると、次のようになります:

from rest_framework import serializers

# Authorシリアライザ
class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']

# Postシリアライザ
class PostSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()  # ネストされたAuthorシリアライザ

    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'author', 'created_at', 'updated_at']

ご覧の通り、ModelSerializerを使用することで、わずかなコードでシリアライザの定義を完了でき、手動設定の重複と手間を避けることができます。


ModelSerializerの懸念点#

しかし、そのような利便性は一定の懸念点ももたらします。

自分でフィールドを定義する必要がないため、ModelSerializerは多くのフィールドの暗黙的な変換(Djangoモデルフィールドからシリアライザフィールドへの変換)を行ってくれます。

なぜ「暗黙的」なのでしょうか。自動変換されたシリアライザフィールドの型や特性、読み取り専用(read_only)かどうかといった詳細を、開発者が必ずしも把握できないからです。

言い換えれば、ModelSerializerは自動的にフィールドを生成するだけでなく、フィールドの型、属性、属性のパラメータなども自動的に推論します。

具体例による説明#

このように言うと少し抽象的で、DRFを書いたことがない読者には理解しにくいかもしれません。直接例を見てみましょう:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
これは非常にシンプルなDjangoモデルで、Django公式ドキュメントから引用しました。

これにはfirst_namelast_nameの2つのフィールドがありますが、実際にはDjangoが自動生成したidフィールドもあり、コード上には表示されていません。

ModelSerializerの「魔法」#

ModelSerializerを使用して、次のようにシリアライザを定義できます:

from rest_framework import serializers
from .models import Person

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ['id', 'first_name', 'last_name']

コードは非常にシンプルですが、その背後には多くの「魔法」があります。

なぜなら、実際のシリアライザとフィールドはこのようになっているからです:

from rest_framework import serializers

class PersonSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    first_name = serializers.CharField(max_length=30)
    last_name = serializers.CharField(max_length=30)

    def create(self, validated_data):
        return Person.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.first_name = validated_data.get('first_name', instance.first_name)
        instance.last_name = validated_data.get('last_name', instance.last_name)
        instance.save()
        return instance
少し驚きませんでしたか?

暗黙的な変換が多くのことを行う#

その中で、idフィールドには自動的にread_only=Trueが追加され、first_namelast_nameには自動的にmax_length=30が追加されています。

これはDjangoモデルの設計とフィールドパラメータが比較的シンプルな場合の話であり、Modelのフィールドがより複雑になると、ModelSerializerの「魔法」もさらに複雑になります。

その背後には多くの変換ロジックがあり、開発者は特定の状況でこれらの「隠されたルール」を理解する必要に迫られます。自動推論が要件に合わず手動でオーバーライドしなければならない場合があるからです。

要するに、自動推論と変換は手動設定の手間を省く一方、細部を調整したり具体的な変換ロジックを理解したりする場面では、この暗黙的な振る舞いが混乱を招く可能性があります。

魔法の代償#

実際の開発では、このような暗黙的変換の「魔法」によって、開発者は変換プロセスを理解し、制御しにくくなります。シリアライズの結果が想定と完全には一致しないことも少なくありません!

この時、私たちはしばしばDRFの公式ドキュメントを参照して、内部でこれらのフィールド変換がどのように処理されているかを理解する必要がありますが、すべての詳細が明確に記載されているわけではありません。

開発者にとって、特に複雑なAPIを処理する場合、学習と保守のコストが明らかに増加します。

以上がまさに私の経験です!

DRFを2年間書いていても、シリアライズの問題に直面した際、私は依然としてドキュメントを再確認する必要に迫られることがよくあります


ModelSchema#

Django NinjaのModelSchemaは、ModelSerializerと比較すると、ずっと「簡素」に見えます。

どういうことでしょうか?公式ドキュメントの例を見てみましょう:

from django.contrib.auth.models import User
from ninja import ModelSchema

class UserSchema(ModelSchema):
    class Meta:
        model = User
        fields = ['id', 'username', 'first_name', 'last_name']

# Will create schema like this:
#
# class UserSchema(Schema):
#     id: int
#     username: str
#     first_name: str
#     last_name: str

それが簡素だというのは、フィールドの「」を自動変換・定義するだけだからです。例えばmax_lengthのようなその他のフィールドの詳細は、Fieldを用いて設定しなければなりません——ModelSchemaはこれらのことを代わりに行ってはくれません

一方、DRFのModelSerializerは、前述のように「より多くのこと」を行います。

ModelSchemaの自動変換が比較的単純であるにもかかわらず、なぜ私はやはり使用を推奨しないのでしょうか?2つの理由があります。

その1つ目の理由が、タイトルで述べた「なぜ私がDjango Ninjaをより好むのか」という理由です。


理由1:結合度の低さと「暗黙より明示」#

Django Ninjaは開発者がAPI構造を把握することをより強調していますが、DRFは高度に統合された便利なツールを提供することに偏っています。

この違いは、彼らがDjangoモデルのシリアライズをどのように扱うかに反映されており、開発者がこれら2つのフレームワークを使用する際のスタイルと思考様式にも影響を与えています。

Django REST frameworkとDjangoの密結合#

DRFはほぼ「Djangoのために高度にカスタマイズされた」API開発ツールであることが分かります。

このような緊密な結合は利便性をもたらす一方で、DRFがDjangoの内部構造と機能に大きく依存していることも意味します。Generic viewsであれ、この記事のModelSerializerであれ、それは同じです。

密結合の利点は多くの作業を省略できることであり、代償は自分が何をしているのかをよく理解していなければならないことです。

暗黙的よりも明示的#

DRFと比較して、Django NinjaとDjangoの結合度はずっと低いです。

私から見れば、Django Ninjaは「暗黙的よりも明示的であること」を好みます。Django Ninjaのスキーマ定義はPydanticに基づいており、開発者に対して入力であれ出力であれ、各フィールドを明確に定義することを要求します。

これは比較的煩雑ではありますが、それがもたらす利点は明らかです。

明示的であることの2つの大きな利点#

第一に、スキーマを手動で定義することで、開発者はデータ構造に対する絶対的な制御権を持つことになります。隠されたルールやブラックボックスの操作はなく、すべてが明確に見えます。

第二に、この方法はモデル層とAPI層の間の結合を効果的に低下させます。実際の開発において、モデルの設計は要件の変化に伴って更新される可能性がありますが、それが直接APIに影響を与えるべきではありません。

総じて、Django Ninjaはスキーマを中核とした制御を強調し、APIの設計をより安定かつ柔軟にし、開発者にデータフローの完全な制御を与えます。


理由2:質が高く読みやすいAPIドキュメント#

第18回では、スキーマのフィールド設定がAPIドキュメントに与える影響について詳しく議論します。

簡単に言えば、ModelSchemaを使用すると、レンダリングされるAPIドキュメントはかなり簡素なものになってしまいます。

これは、APIドキュメントの明確さと正確性に対する私の追求には合致しません


結語#

否定できないことですが、Django REST frameworkには非常に便利で親切な設計があります。例えば前回の記事で言及したsource=パラメータは、直感的でエレガントです。

一方、Django Ninjaは開発者に対し、可能な限り各フィールドを手動で定義し、モデルとAPI層の結合を減らすことを要求します。これはPythonの哲学における「暗黙的よりも明示的であること」により合致しており、同時に暗黙的な振る舞いがもたらす潜在的な問題を回避します。

これがまさに、私がDjango Ninjaをより好む理由です。

Django Ninjaの明示性への追求のおかげで、私はAPIの開発と保守を行う際、多くの場合においてよりリラックスできていると感じています