資料驗證(上)Pydantic 單一欄位驗證
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 19 篇。進入第五章:資料驗證與錯誤處理。
資料驗證是 API 開發中的關鍵需求之一,它負責確保從客戶端提交的資料是符合預期的,從而避免潛在的錯誤和安全問題。
有效的資料驗證可以在 API 接收到錯誤資料時,給出即時且友善的回應,提升系統的穩定性和使用者體驗。
Django Ninja 中,資料驗證的核心工具是 Pydantic。它提供了強大的驗證功能,不僅能對資料型別進行檢查,還能輕鬆實現自定義驗證。
本文將介紹如何在 Django Ninja 中使用 Pydantic 實作單一欄位的自定義驗證;下一篇則講述跨欄位的自定義驗證。
本文所有的程式碼變動,可參考這個 PR。
第五章總論
資料驗證很重要,而驗證失敗時,程式往往會拋出驗證錯誤。如何有效處理這些錯誤,則是「錯誤處理」要討論的範疇。
本章將探討這兩個密切相關的主題,共計 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,對單一欄位進行資料驗證,實作了密碼強度檢查規則。
下一篇,我們要繼續這個主題,實現更複雜的跨欄位驗證。