資料驗證(上)Pydantic 單一欄位驗證
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 19 篇。進入第五章:資料驗證與錯誤處理。
資料驗證是 API 開發中的關鍵需求之一,它負責確保從客戶端提交的資料是符合預期的,從而避免潛在的錯誤和安全問題。
有效的資料驗證可以在 API 接收到錯誤資料時,給出即時且友善的回應,提升系統的穩定性和使用者體驗。
Django Ninja 中,資料驗證的核心工具是 Pydantic。它提供了強大的驗證功能,不僅能對資料型別進行檢查,還能輕鬆實現自定義驗證。
本文將介紹如何在 Django Ninja 中使用 Pydantic 實作單一欄位的自定義驗證;下一篇則講述跨欄位的自定義驗證。
本文所有的程式碼變動,可參考這個 PR。
快速導覽
👉 完整系列目錄:點此查看
👉 程式碼範例:GitHub 範例專案
第五章總論
資料驗證很重要,而驗證失敗時,程式往往會拋出驗證錯誤。如何有效處理這些錯誤,則是「錯誤處理」要討論的範疇。
本章將探討這兩個密切相關的主題,共計 4 篇文章:
- 卷 19:資料驗證(上)Pydantic 單一欄位驗證(本文)
- 卷 20:資料驗證(下)Pydantic 跨欄位驗證
- 卷 21:錯誤處理(上)HttpError 與自定義 HTTP 回應
- 卷 22:錯誤處理(下)全域錯誤處理——使用 Exception Handlers
前兩篇,我們會學習如何實現靈活的資料驗證,以確保輸入資料符合預期,並在必要時拋出錯誤。
後兩篇,我們將討論如何處理 API 流程中可能出現的各種錯誤(不限於驗證錯誤),以提供更好的使用者體驗。
Django Ninja 的資料驗證與錯誤處理機制,相較 Django REST framework 更加複雜,因此我們得用完整的篇幅來介紹,幫助你清楚地理解它們。
API 修正
我們會以上一篇文章中新建立的 API——新增使用者——為例。
繼續改善它,加上自定義驗證,讓客戶端傳來的資料更可靠。
不過我要先做一些錯誤修正,修正後的程式碼如下:
1 |
|
主要改正了這兩處:
- 在
router裝飾器新增了response={201: dict}參數。本來沒有定義,實際使用這個 API 時會出現錯誤。因為預設只有 200 回應,想要 200 以外的回應,要透過response參數聲明才行。 - 使用
set_password方法對用戶輸入的密碼進行加密。這是 Django 內建的功能,防止密碼直接儲存在 db 中。密碼不能明文存儲,無疑是現代開發的 ABC。
修正結束,我們正式進入主題。
不同「層次」的驗證
既然是驗證,主要當然是跟來自客戶端的請求有關——驗證請求內容。
Django Ninja 中,每個 API 可以透過定義 Schema,來描述 API 所接收的資料結構。這些 Schema 基於 Pydantic,能自動對請求中的資料進行驗證。
Schema 中的 type hints 可以驗證資料型別,這是最基本的驗證。
前一篇提到的 Pydantic Field,則可以對資料的長度、範圍等特性進行驗證。這部分在後面會示範。
這些都是偏「形式上」的驗證,而本文將聚焦於更複雜的「自定義驗證」——基於一定的規則。
範例 API 的 Schema 現狀
以「新增使用者」為例,request body 接收username、email、password和bio等欄位。透過我們定義的 Schema,能完成最基本的資料型別驗證。
1 | class CreateUserRequest(Schema): |
如上一篇所述,只有bio欄位是可選的,其餘則為必填——缺少就會得到 422 回應。所以 Schema 同時也驗證了資料的「存在性」。
目前看起來還不錯!但我們並不就此滿足。
新需求:密碼規則
我們要求使用者在設定密碼時,遵守以下兩個規則:
- 密碼長度至少 8 個字元。
- 必須包含至少一個數字。
這些規則有助於提高帳號的安全性,防止用戶設定過於簡單的密碼。
考慮到教學目的,我沒有讓規則過於複雜。這兩條規則都有其特定的教學意義:
- 最小長度限制可以直接透過 Pydantic Field 實現,不必自行實作。
- 第二個規則是重頭戲,我們會使用 Pydantic 的
@field_validator裝飾器,自行定義欄位的驗證規則。
實作密碼規則驗證:使用 field_validator
根據需求,我們可以先利用 Pydantic 的Field來設定最小長度限制:
1 | password: str = Field(min_length=8, examples=['password123']) |
如上,我們只需要新增一個min_length=8參數即可。
至於「必須包含數字」的驗證,則要用@field_validator裝飾器來實作。
field_validator 裝飾器
在 Pydantic v1 中,這個裝飾器的名稱是validator,v2 才改為field_validator。
Pydantic 從 v1 到 v2,有許多 breaking change,比如之前提過的example參數變成examples,即是一例。這部分值得留意。
以下是修改後的 Schema,我們只關注field_validator部分:
1 | class CreateUserRequest(Schema): |
重點解析
field_validator裝飾器必須使用參數,合法值是欄位名稱,如password。- 雖然範例中沒有演示,但它可以套用在多個欄位。
- 寫法為
@field_validator('欄位1', '欄位2', ...),你甚至可以直接寫成@field_validator('*')——套用到全部欄位。 - 但請注意,這些欄位會執行同一個驗證邏輯,所以它們理論上是邏輯類似的欄位。
- 寫法為
- 驗證方法的名稱可以自訂,你想怎麼命名都行,只要自己好懂即可。
- 因為 Pydantic 主要是看裝飾器上的欄位名稱。
- 這和 Django REST framework 的驗證方法是採用
validate_<欄位名>的命名模式,有很大的不同。
- Pydantic 驗證方法的參數名稱命名慣例是
v,而 Django REST framework 則是value。 - 慣例二:驗證方法在成功時會原封不動 return 輸入值;失敗時則會拋出錯誤。
- Pydantic 的驗證方法是一個「類別方法」,所以第一個參數是
cls。特別的是,你可以省略@classmethod裝飾器,因為 Pydantic 已經在內部處理了。- 不過官方文件仍建議你使用
@classmethod,我們從善如流。 - 如果有聲明
@classmethod裝飾器,它的位置必須最靠近驗證方法。
- 不過官方文件仍建議你使用
想不到吧?短短幾行,竟然有這麼多看點!
實際測試
測試密碼長度不足的情況,結果為:
1 | { |
這是 Field 檢查時自行拋出的錯誤,回應狀態碼為 422。
接下來,測試密碼未包含數字的情況:
1 | { |
這算是由我們「半自定義」的錯誤類回應,因為結構仍是 Django Ninja 決定,但錯誤訊息部分則是我們自己定義的。
對於錯誤回應的自定義還可以更靈活,不過這是下下篇「錯誤處理(上)HttpError 與自定義 HTTP 回應」的主題,到時再來詳細討論。
小結
這一篇,我們學習了如何透過 Pydantic,對單一欄位進行資料驗證,實作了密碼強度檢查規則。
下一篇,我們要繼續這個主題,實現更複雜的跨欄位驗證。