Python Pydantic 資料驗證與設定管理
在 Python 開發中(特別是 Web API 開發,如 FastAPI),資料驗證 (Data Validation) 和解析 (Parsing) 是非常重要的一環。傳統的做法可能需要手動寫很多 if-else 檢查型別和內容,既繁瑣又容易出錯。
Pydantic 是一個基於 Python Type Hints 的資料驗證庫。它讓你用定義類別 (Class) 的方式來宣告資料結構,並自動處理型別轉換和驗證。
為什麼選擇 Pydantic?
- 基於標準 Type Hints:使用你熟悉的
str,int,List,Optional等語法,不需要學習新的 Schema 定義語言。 - 自動型別轉換:如果你定義欄位是
int,但傳入"123"(字串),Pydantic 會試著幫你轉成整數123。 - IDE 支援度高:因為基於 Class 和 Type Hints,自動補全 (Autocomplete) 和靜態檢查 (MyPy/Pyright) 都能完美運作。
- 效能優異:核心部分使用 Rust 編寫,速度極快。
- 設定管理:內建
BaseSettings,非常適合用來讀取環境變數 (Environment Variables) 管理專案設定。
基本用法
使用 Pydantic 非常簡單,只需要建立一個繼承自 pydantic.BaseModel 的類別。
from pydantic import BaseModel, ValidationError
from typing import List, Optional
class User(BaseModel):
id: int
name: str
signup_ts: Optional[str] = None
friends: List[int] = []
# 1. 正常資料 (自動轉換型別)
# 注意 id 傳入字串 "123",會被自動轉成整數 123
user_data = {
"id": "123",
"name": "John Doe",
"signup_ts": "2024-01-01 12:00",
"friends": [1, 2, "3"]
}
user = User(**user_data)
print(user)
print(user.id) # 123 (int)
# 2. 錯誤資料 (驗證失敗)
try:
User(id="not-a-number", name="Jane")
except ValidationError as e:
print(e.json())
# 會輸出詳細的錯誤訊息,指出哪個欄位錯誤、原因為何
與 Python Dataclasses 的差異
雖然 Pydantic 看起來很像標準庫的 dataclasses,但主要區別在於:
- Dataclasses:主要用於儲存資料,不會主動驗證或轉換數據。
- Pydantic:主要用於解析與驗證資料,確保資料符合定義的規範。
- Pydantic:主要用於解析與驗證資料,確保資料符合定義的規範。
巢狀模型 (Nested Models)
Pydantic 支援將一個 Model 作為另一個 Model 的欄位型別,這在處理複雜的 JSON 結構時非常有用。
class Address(BaseModel):
city: str
street: str
zip_code: str
class UserWithAddress(BaseModel):
name: str
# 引用 Address Model
address: Address
data = {
"name": "Bob",
"address": {
"city": "Taipei",
"street": "Xinyi Rd.",
"zip_code": "110"
}
}
user = UserWithAddress(**data)
print(user.address.city) # Taipei
除了基本的型別檢查,我們常常需要更細的限制,例如「年齡必須大於 0」、「字串長度不能超過 50」。這可以使用 Field 來達成。
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(..., max_length=50, description="商品名稱")
price: float = Field(..., gt=0, description="價格必須大於 0")
# gt: greater than
# ge: greater than or equal
# lt: less than
# le: less than or equal
quantity: int = Field(1, ge=1) # 預設值為 1,且必須 >= 1
try:
Item(name="Apple", price=-5)
except ValidationError as e:
print(e)
# price
# Input should be greater than 0 [type=greater_than, input_value=-5, input_type=int]
使用 Annotated 定義限制 (推薦)
在 Pydantic V2 中,更推薦使用 Python 標準庫的 typing.Annotated 來定義欄位限制。這樣可以把「型別定義」和「預設值」分開,程式碼會更乾淨,也更 conform Python 的 Type Hint 標準。
from typing import Annotated
from pydantic import BaseModel, Field
class Item(BaseModel):
# 把 Field 限制放在 Annotated 裡
name: Annotated[str, Field(max_length=50)]
# 預設值可以單獨寫在等號後面,不用擠在 Field 裡
price: Annotated[float, Field(gt=0)] = 0.0
quantity: Annotated[int, Field(ge=1)] = 1
print(Item(name="Banana", price=10.0))
欄位別名 (Field Aliases) 與駝峰式命名
在 Python 中我們慣用 snake_case (如 first_name),但 API 的 JSON 資料常使用 camelCase (如 firstName)。Pydantic 提供了 Alias 功能來解決這個問題。
from pydantic import ConfigDict
class CamelCaseModel(BaseModel):
# 允許使用 name 或 serial_no 初始化
# validation_alias: 讀取輸入資料時使用的別名
# serialization_alias: 輸出資料 (dump) 時使用的別名
first_name: str = Field(validation_alias="firstName")
# 也可以直接在 Config 中設定全域規則 (V2 推薦寫法)
model_config = ConfigDict(populate_by_name=True)
data = {"firstName": "John"}
m = CamelCaseModel(**data)
print(m.first_name) # John
如果 Field 的內建限制不夠用,可以使用 @field_validator 裝飾器來寫自定義邏輯。
After Validator (預設)
這是在 Pydantic 完成型別轉換 之後 執行的驗證。例如欄位是 int,Pydantic 會先嘗試把字串轉成整數,成功後再丟給你的 validator 檢查。
from pydantic import BaseModel, field_validator
class RegisterUser(BaseModel):
username: str
@field_validator('username')
@classmethod
def username_must_be_alphanumeric(cls, v: str) -> str:
# 這裡收到的 v 已經確保是 str 型別
if not v.isalnum():
raise ValueError('使用者名稱只能包含英數字')
return v.title() # 也可以修改值
try:
RegisterUser(username="user_name!!")
except ValidationError as e:
print(e)
# Value error, 使用者名稱只能包含英數字 [type=value_error, input_value='user_name!!', input_type=str]
Before Validator (資料預處理)
有時候你需要在 Pydantic 進行型別轉換 之前 就介入處理。例如:前端傳來的日期格式很奇怪,或者你想允許傳入 "Yes"/"No" 字串來代表 True/False。
這時可以使用 mode='before'。
from typing import Any
from pydantic import BaseModel, field_validator
class CheckBox(BaseModel):
is_checked: bool
@field_validator('is_checked', mode='before')
@classmethod
def check_legacy_boolean(cls, v: Any) -> Any:
# 因為是 before validator,v 可能是任何型別 (raw input)
if isinstance(v, str):
if v.lower() == 'yes':
return True
if v.lower() == 'no':
return False
return v # 回傳處理後的值,接著交給 Pydantic 做標準型別驗證
c1 = CheckBox(is_checked="Yes")
print(c1.is_checked) # True (bool)
c2 = CheckBox(is_checked="no")
print(c2.is_checked) # False (bool)
模型驗證器 (Model Validators)
Field Validator 只能檢查「單一欄位」,但有時候我們需要驗證「多個欄位之間的關係」,例如:「確認密碼」必須等於「密碼」、「如果選擇信用卡付款,卡號欄位不能為空」。
這時就要用 @model_validator。
from typing import Self
from pydantic import BaseModel, model_validator, ValidationError
class ChangePassword(BaseModel):
password: str
confirm_password: str
@model_validator(mode='after')
def check_passwords_match(self) -> Self:
# 這裡的 self 已經是填好資料的 Model instance
if self.password != self.confirm_password:
raise ValueError('兩次輸入的密碼不一致')
return self
try:
ChangePassword(password="secret", confirm_password="wrong")
except ValidationError as e:
print(e)
# Value error, 兩次輸入的密碼不一致 [type=value_error, input_value=..., input_type=ChangePassword]
mode='before' 的 Model Validator 則是用來處理「進入 Model 之前」的整個 Dictionary 資料,適合用來做資料清洗或結構攤平 (Flattening)。
計算欄位 (Computed Fields)
有時候某個欄位的值是依賴其他欄位計算出來的,例如 full_name 是由 first_name + last_name 組成。Pydantic V2 提供了 @computed_field 裝飾器。
from pydantic import computed_field
class Person(BaseModel):
first_name: str
last_name: str
@computed_field
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
p = Person(first_name="Tom", last_name="Cruise")
print(p.full_name) # Tom Cruise
# dump 時也會包含 computed fields
print(p.model_dump())
# {'first_name': 'Tom', 'last_name': 'Cruise', 'full_name': 'Tom Cruise'}
嚴格模式 (Strict Mode)
預設情況下,Pydantic 會嘗試進行型別轉換(例如字串 "123" 轉整數 123)。如果你希望禁止這種行為,確保輸入型別必須完全符合,可以使用 Strict Mode。
from pydantic import ConfigDict
class StrictUser(BaseModel):
model_config = ConfigDict(strict=True)
id: int
name: str
try:
# 這裡會報錯,因為 id 必須是 int,不能是 str
StrictUser(id="123", name="Alice")
except ValidationError as e:
print(e)
# Input should be a valid integer [type=int_type, input_value='123', input_type=str]
當你需要將 Pydantic 物件轉回 Dictionary 或 JSON 字串(例如 API 回傳資料時),可以使用 model_dump() 和 model_dump_json()。
user = User(id=1, name="Alice")
# 轉成 Dictionary
user_dict = user.model_dump()
print(user_dict)
# {'id': 1, 'name': 'Alice', 'signup_ts': None, 'friends': []}
# 轉成 JSON String
user_json = user.model_dump_json()
print(user_json)
# {"id":1,"name":"Alice","signup_ts":null,"friends":[]}
dict() 和 json(),但在 V2 中已被棄用 (Deprecated),建議改用新的名稱。更多建立模型的方式
除了使用 User(**data) 這種像是 keyword arguments 的方式建立物件,Pydantic 還有提供專門的方法來讀取資料:
Model.model_validate(dict_data): 從 Dictionary 建立Model.model_validate_json(json_str): 從 JSON 字串直接建立 (效率比先json.loads再丟進去更好)
# 從 JSON 字串建立
json_input = '{"id": 100, "name": "Bob"}'
user = User.model_validate_json(json_input)
print(user.name) # Bob
設定管理 (Generic Settings)
Pydantic 另一個強大的功能是 BaseSettings (需安裝 pydantic-settings 套件),它可以用來管理整個應用程式的設定,並支援從環境變數 (Environment Variables) 自動讀取。
安裝:
pip install pydantic-settings
使用範例:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "My App"
admin_email: str
items_per_user: int = 50
# 會自動讀取 .env 檔案
class Config:
env_file = ".env"
# 如果環境變數中有 ADMIN_EMAIL,會自動覆蓋
# export ADMIN_EMAIL="admin@example.com"
settings = Settings()
print(settings.app_name)
print(settings.admin_email) # 如果沒讀到環境變數會報錯
這在開發雲端應用程式(符合 12-factor app 原則)時非常實用,你不需要自己寫程式碼去解析 os.environ。
總結
Pydantic 已經成為現代 Python 開發的標準配備之一,特別是在數據密集型的應用中。它讓資料驗證變得宣告式、可讀性高且強型別,大幅提升了程式碼的健壯性。