為何不用 ModelSchema?#

這是 Django Ninja 系列教學的第 15 篇。
Django API 回應,常常是對 Model 物件(即 db 資料)內容進行一定的篩選與加工。
比如「取得單一文章資訊」API,實際上就是從 Post 物件挑選欄位,再進行序列化。
這個過程中,我們需要考慮如何將模型物件轉換為 API 的回應結構,同時保持程式碼的可維護性與靈活。
對此,Django REST framework(以下簡稱 DRF)提供了非常實用的「特製」序列化器——ModelSerializer,可說是 DRF 開發者必學的核心功能。
Django Ninja 雖然也有類似的實踐——ModelSchema,對我而言卻是雞肋般的存在,我幾乎不曾使用。
這樣的差異,無疑是兩者的核心設計理念不同所導致。
我們曾在第 3 篇中討論過,兩者在功能上的主要區別。本文將透過「Django 模型物件的序列化」這個頗具代表性的議題,說明「為何相比於 DRF,我更喜歡寫 Django Ninja」。
範例專案#
ModelSerializer 的亮點#
DRF 中的 ModelSerializer 是個非常強大的工具,它能夠自動將 Django 模型轉換為 API 需要的資料結構——序列化器,大大簡化了「為序列化器定義欄位」的過程。
附帶一提,DRF 序列化器,相當於 Django Ninja 所使用的 Schema,兩者的概念大同小異,都是用於資料的驗證與序列化。
如果我們把「取得單一文章資訊」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 Model 欄位轉換為序列化器欄位。
為何說「隱式」呢?因為自動轉換後的序列化器欄位,其欄位的型別、特性、是否唯讀(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 Model,我引用自 Django 官方文件。
它有兩個欄位 first_name 和 last_name,實際上它還有一個 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_name 和 last_name 則被自動加上了 max_length=30。
這還是在 Django Model 的設計與欄位參數相對簡單的情況下,當 Model 欄位更複雜時,ModelSerializer 的「魔法」也會變得更加複雜。
它背後有很多轉換邏輯,讓開發者在某些情況下必須去理解這些「隱藏規則」——因為這個推斷有時可能不符合你的需求,導致你需要手動覆寫。
總之,自動推斷與轉換固然省去了手動設定的麻煩,但當你需要調整某些細節,或理解具體的轉換邏輯時,這種隱式行為可能會讓你感到困惑。
魔法的代價#
在實際開發中,這種隱式轉換的「魔法」會讓開發者失去對轉換過程的理解與掌控。你很可能會發現,序列化的結果和你想的並不完全一致!
此時我們往往需要翻閱 DRF 的官方文件來理解內部如何處理這些欄位轉換,但也不是每個細節都寫得清楚明白。
對開發者而言,特別是在處理複雜 API 時,會明顯增加學習和維護成本。
以上正是我的經驗!
即使寫了 2 年 DRF,遇到序列化問題,我還是很常需要重新查看文件。
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 的自動轉換相對單純,那為何我還是不建議使用呢?有兩個理由。
其中第一個理由,就是標題所說「為何我更偏愛 Django Ninja」的理由。
理由一:低耦合 + 明確優於隱晦#
Django Ninja 更強調開發者對 API 結構的掌握,而 DRF 則偏向於提供高度整合且便利的工具。
這種差異反映在它們對待 Django 模型序列化的方式上,也影響了開發者在使用這兩個框架時的風格和思維方式。
Django REST framework 和 Django 高度耦合#
我們可以發現, DRF 幾乎是一個「為 Django 高度定製」的 API 開發工具。
這種緊密的結合雖然帶來了便利性,但也意味著 DRF 在很大程度上依賴於 Django 的內部結構和功能。不管是 Generic views,還是本文的 ModelSerializer,都是如此。
高耦合的優點就是你可以少做很多事,而代價則是你要很了解自己在做什麼。
明確優於隱晦#
相較於 DRF,Django Ninja 與 Django 的耦合程度則要低得多。
在我看來,Django Ninja 更偏好「明確優於隱晦」,Django Ninja 的 Schema 定義是基於 Pydantic,它要求開發者明確定義每個欄位,無論是輸入還是輸出。
雖然這樣相對繁瑣,但它帶來的好處是顯而易見的。
明確的兩大優點#
首先,手動定義 Schema 讓開發者對資料結構有著絕對的掌控權。沒有任何隱藏規則或暗箱操作,一切都清晰可見。
其次,這種方法有效地降低了模型層與 API 層之間的耦合。在實際開發中,模型設計可能會隨著需求變化而更新,但這不應該直接影響到 API。
總的來說,Django Ninja 強調以 Schema 為核心的控制,讓 API 的設計更具穩定性和靈活性,並賦予開發者對資料流的完全掌控。
理由二:更好、更可讀的 API 文件#
在第 18 篇,我們會詳細討論 Schema 欄位設定對 API 文件的影響。
簡言之,如果使用 ModelSchema,那麼渲染出來的 API 文件將會相當陽春。
這並不符合我對 API 文件清晰與明確性的追求。
結語#
不可否認,Django REST framework 有一些非常方便且貼心的設計,比如上一篇提到的 source= 參數,它直觀而優雅。
Django Ninja 則要求開發者,盡可能手動定義每個欄位,減少模型與 API 層的耦合,這更符合 Python 哲學中的「明確優於隱晦」,同時避免隱式行為帶來的潛在問題。
這正是我更偏愛 Django Ninja 的原因。
Django Ninja 對明確性的追求,讓我在開發和維護 API 時,多數時候感覺更加輕鬆。