資料驗證(下)Pydantic 跨欄位驗證
2024 iThome 鐵人賽
這是 Django Ninja 系列教學的第 20 篇。
上一篇我們講完了單一欄位的自定義驗證,這篇則要來討論跨欄位之間的驗證。
跨欄位驗證同樣是 API 開發中十分常見的需求,例如註冊帳號時,要保證「密碼」與「確認密碼」兩個欄位內容相同;選擇日期期間時,開始日期不能晚於結束日期等。
這些驗證場景無法透過單一欄位驗證實現,因為它們需要同時檢查多個欄位之間的邏輯關聯,來確保整體資料的一致性和正確性。
本文將介紹如何透過 Pydantic 來實現跨欄位驗證需求——以「確認密碼」為例,展示這個功能的實際應用。
本文所有的程式碼改動,可參考這個 PR。
跨欄位驗證與關注點分離
其實,無論是單一欄位還是跨欄位的自定義驗證,都不一定要藉由 Pydantic 來完成。
理論上,資料驗證可以直接在 view 函式中進行,例如取出輸入的欄位值,手動驗證它的合法性。跨欄位驗證也是如此。
然而,這是一種方便但「粗糙」的做法——只適合用在驗證邏輯非常單純的情況。
透過 Pydantic 進行資料驗證,則能夠帶來一個明顯的好處:關注點分離。
關注點分離
關注點分離(Separation of Concerns)是一種設計原則。主張將程式中不同功能的職責劃分到獨立的模組或層次中。
每個模組主要專注於一個具體的方向或目標,從而避免把多個不同的功能耦合在一起。這樣的劃分可以讓程式更易於測試、維護和擴充。
依照關注點分離,資料驗證的邏輯應該集中在 Schema,而不是在 view 函式中進行。
如此一來,view 可以專注於處理核心業務邏輯,而將資料驗證交由專門的元件負責。
透過 Pydantic 的驗證機制,我們可以實現關注點分離,讓資料驗證與業務邏輯分開,這不僅提升了程式碼的結構,也讓開發流程更加清楚、穩定。
新需求:確認密碼
我們要實作一個非常簡單,但足以充分說明跨欄位驗證價值的功能:確認密碼。
先回顧上一篇結束時,「新增使用者」API 的請求 Schema 內容:
1 | class CreateUserRequest(Schema): |
這個 Schema 的設計,顯然有所不足。
因為用戶註冊時,密碼通常需要輸入兩次,第二次的作用是「確認」——重要的事情說兩次嘛!
所以,我們要新增一個confirm_password
欄位,和password
進行跨欄位驗證:確認兩者內容相同。
儘管其中的驗證邏輯非常簡單,但這正是跨欄位驗證的絕佳舞台。
實作跨欄位驗證:使用 model_validator
Pydantic v2 引入了@model_validator
裝飾器來處理跨欄位驗證,這是對 Pydantic v1 中@root_validator
的改進和替代。
這裡的 model,指的是 Pydantic 的 BaseModel——也就是我們的 Schema,而不是 Django 的 Models。
我們透過@model_validator
來強化「新增使用者」API,加上「確認密碼」功能。
直接看修改後的程式碼:
1 | class CreateUserRequest(Schema): |
重點解析
- 新增了一個
confirm_password
欄位。 - 使用
@model_validator(mode='after')
裝飾器來定義跨欄位的驗證方法。mode
總共有三種:before、after 和 wrap。其中的細節頗多,限於篇幅,本文無法展開(可能等番外篇再行補充)。- 你只要知道,大部分時候是用 after 模式,此時的驗證方法是一個「實例方法」,
self
參數代表 Schema 實例本身(從 input 資料初始化而來)。
- 驗證方法
check_passwords_match
比較password
和confirm_password
欄位,如果欄位內容不相同,則拋出ValueError
。- 如前所述,儘管邏輯非常簡單,但它確實現了兩個欄位之間的驗證。
- 跨欄位驗證在所有單一欄位驗證完成後才會執行。
關注點分離的實際應用
你會發現,在這次新增「確認密碼」的功能實作中,view 函式完全沒有變動!——這正是關注點分離原則的體現。
相較於直接在 view 函式中實作驗證邏輯(需要同時修改 view 和 Schema),這樣的實作方式無疑更加乾淨、解耦。
驗證失敗時的 HTTP 回應
最後,讓我們來看看,當資料驗證失敗時,會得到什麼樣的 HTTP 回應。
前兩項是上一篇已經提過的,這裡再次列出,以便相互對照及複習。
違反密碼長度限制
回應結果如下:
1 | { |
這是 Django Ninja 捕捉 Pydantic 驗證錯誤所給出的「系統級」回應,狀態碼為 422。
違反「必須包含數字」規則
輸入的密碼中沒有數字,回應結果如下:
1 | { |
好像差不多耶?沒錯,因為這也是 Django Ninja 的自動回應格式——除了錯誤訊息中包含我們自定義的內容。
拋出 ValueError 的回應
但事實上,這是因為我們在驗證方法中拋出的是ValueError
,所以 Django Ninja 會自動幫你處理。
類似的回應也發生在確認密碼不一致時:
1 | { |
那如果拋出的別種錯誤,比如 Django 的ValidationError
,甚至是我們自己定義的錯誤,Django Ninja 還會自動處理嗎?
答案是:不會。
你會得到「500 Internal Server Error」——這將是我們下下篇的重點。
小結與下一步
本文中,我們介紹了如何透過@model_validator
來實現跨欄位驗證的需求,同時落實關注點分離原則。
學習完這兩篇以後,你對 Django Ninja 資料驗證的了解,已經超越大部分人。
接下來,我們將深入探討,當資料驗證失敗時,要如何優雅地處理錯誤——並回應,以提升 API 的使用體驗。
相關文章