回應(四)Resolver 方法——欄位資料格式化
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 16 篇。
上一篇提到,API 回應常常是對 Django Model 物件內容的篩選與加工——然後 JSON 序列化。
其中「加工」部分,用更專業的說法,大概是「資料格式化」——依照一定的規則,對輸出資料進行某種轉換或重新組織,以符合特定的輸出格式。
資料格式化的種類很多,例如:
- 時間格式轉換:將資料庫中的時間戳(timestamp),轉換為更易讀的格式。
- 數值轉換:將數字轉換為貨幣格式,或將小數點位數進行四捨五入。
- 字串處理:截斷過長的文字、加上統一的前綴等。
不論原因為何,絕大部分時候都是為了資料的「可讀性」,或符合特定業務規則。
可想而知,像資料格式化這樣的需求,不僅實務上重要,在 API 開發中也十分常見,值得我們用一整篇文章,細細探討。
本文所有的程式碼變動,可參考這個 PR。
場景與需求
再次回到「取得單一文章資訊」API,這是目前的回傳格式:
1 | // http://127.0.0.1:8000/posts/2/ |
我們決定簡化回應的時間字串,改採「"2024-09-12T02:28:16Z"
」格式。
和舊版相比,只是少了「.801
」這個小數部分而已,且依舊符合 ISO 8601 標準。
總之,回應中created_at
和updated_at
兩個欄位的內容,需要進行格式上的轉換。即上述提到的「資料格式化」。
Django REST framework 做法
首先,我們還是不免俗地先介紹 Django REST framework(以下簡稱 DRF)的做法,方便你對比兩者的差異——你會發現其實大同小異。
在 DRF 中,我們可以透過SerializerMethodField
實現時間格式的轉換。以下是透過 DRF 實現的範例:
1 | class PostSerializer(serializers.ModelSerializer): |
其中的重點有三:
- 要格式化的欄位,值必須是
SerializerMethodField
。 - 在序列化器類別中,定義相同欄位名稱的實例方法(有第一位置參數
self
),且命名時要加上get_
前綴,比如get_created_at
。 obj
參數指的是當前被序列化的物件。本例中,我們預期引數是一個Post
模型實例。這個方法將在序列化過程中會被自動調用,將原始的 datetime 物件轉換為指定的字串格式。
附帶一提,在 DRF 序列化器的各種實例方法中,obj
這個參數名稱可以稱得上是一個命名慣例。
Django Ninja 的欄位資料格式化
看完 DRF,我們來看看 Django Ninja 怎麼做。
透過 Django Ninja 的 Resolver 方法,我們也能輕鬆處理這類需求。
Django Ninja 的 Resolver 方法
在 Django Ninja 中,我們用 Resolver 方法來實現同樣的功能:
1 | class PostResponse(Schema): |
方法的命名,相較於 DRF 用的是get_
前綴,Django Ninja 則是採resolve_
前綴。
此外,你沒有看錯,這裡使用了兩種寫法:
resolve_created_at
是一個「靜態方法(static method)」,需要使用@staticmethod
裝飾器,且沒有self
參數。resolve_updated_at
是一個典型的實例方法,有self
參數。
因為文件的範例中,確實存在這兩種寫法:
1 | class TaskSchema(Schema): |
實例方法版本未實裝
但是!現階段,你只要知道「靜態方法」版本即可。
因為採第二種寫法,你將會得到下列錯誤訊息:
Error extracting attribute: NotImplementedError: Non static resolves are not supported yet [type=get_attribute_error, input_value=<DjangoGetter: <Post: Ali…’s Django Ninja Post 1>>, input_type=DjangoGetter]
什麼?還沒有實作!
我只好乖乖都改成靜態方法。
回傳結果
最後看一下效果如何:
1 | // http://127.0.0.1:8000/posts/2/ |
很好,時間字串格式已經被成功轉換——是16Z
而不是16.801Z
。
使用 Alias 攤平欄位資訊
另一個常見的格式化需求,是我們之前提過的「攤平」(flatten)複雜資料結構。
這是一種對資料的「重組」,而結構重組同樣屬於本文所探討的資料格式化範疇。
還記得在第 14 篇,我們透過@property
產生「取得文章列表」回應中author_name
欄位內容嗎?——這是對User
模型的攤平,直接獲取其username
欄位資訊。
這裡我們換一個更優雅的做法——alias。
使用 Alias
Django Ninja(幾乎是從 Pydantic 照搬來的)提供了Field
與alias
參數來實現這一功能。
有關Field
,在〈卷 18:Pydantic Field 設定範例與預設值〉將會有更多著墨。
我們先來看看如何使用:
1 | class PostListResponse(Schema): |
注意,原先Post
模型的@property
方法要拿掉,或至少不能和author_name
撞名,否則會出錯唷!
我選擇了移除@property
方法,直接改用這個新做法。
重點解析
透過alias=author.username
取得Post
的關聯模型——User
的username
屬性值。實現了巢狀資料的攤平。
這種設計,顯然是向 DRF 的優秀借鑑,相當於 DRF 中的 source=author.username
寫法。
雖然有點抽象,卻非常優雅。
alias
的用途不限於資料攤平(這反而是比較進階的用法),其它細節,如欄位名稱替換等,可直接參考 Pydantic 文件。
回傳結果
這個做法,和之前使用@property
的效果是完全一樣的:
1 | // http://127.0.0.1:8000/posts/ |
可以看到author_name
欄位已經成功攤平,直接顯示了作者的名字。
結語
Django Ninja 的 Resolver 方法允許我們對 API 回應中的欄位資料進行動態處理,滿足各種格式轉換與自定義需求。
在處理像created_at
和updated_at
這樣的時間欄位時,Resolver 方法不僅簡單易用,還能保證程式碼的結構清晰。
Field
與alias
參數則更優雅地實現了另一種常見的資料格式化——「攤平」。不僅簡化了 API 回應,且無需修改背後的 Django 模型。
透過這些方式,我們能更靈活地控制 API 的輸出,以符合客戶端需求。
下一章預告
完成了對「Django Ninja 處理 HTTP 回應」共 4 篇的學習,第三章也正式告一段落。接下來,我們要將目光轉向 API 開發中的另一個重要主題——文件!
隨著專案規模的增長,清晰的 API 文件對於任何需要使用 API 的人員都至關重要——包括後端開發者自己!
一份好的 API 文件能夠大幅降低溝通成本,提高開發效率、減少錯誤。它不僅是一種技術文書,更是團隊協作的重要樞紐。
第四章,我們會探討如何有效地透過 Django Ninja 程式碼,產生高品質的 API 文件,從而提升整體的開發體驗。