如果你寫過 Java 或 TypeScript,你一定遇過這個問題:型別系統只在編譯時期保護你,runtime 進來的資料(API request、JSON 檔、資料庫查詢結果)依然可能不符預期。
Pydantic 解決的就是這件事——它讓 Python 的 type hints 從「給 IDE 看的註解」變成「執行期強制驗證的合約」。
一句話定位
Pydantic = 用 Python type hints 宣告資料模型,在物件建立時自動驗證、轉型,失敗就丟出結構化的錯誤。
對照你可能熟悉的工具:
| 生態系 | 對應工具 | 角色 |
|---|---|---|
| Java | Bean Validation + Jackson | 欄位驗證 + JSON 序列化 |
| TypeScript | Zod | runtime schema 驗證 |
| Python | Pydantic | 兩者合體 |
安裝
pip install pydantic
本文以 Pydantic v2 為準(API 與 v1 有不少差異,v2 核心以 Rust 重寫,效能大幅提升)。
基本用法:BaseModel
from pydantic import BaseModel, Field
class User(BaseModel):
name: str
age: int = Field(ge=0, le=150) # 數值範圍限制
email: str | None = None # 可選欄位,預設 None
u = User(name="Albert", age="35") # 注意 age 傳入的是字串
print(u.age) # 35 → 已自動轉成 int
print(type(u.age)) # <class 'int'>
兩個關鍵行為:
- 自動轉型(coercion):
"35"會被轉成35。這對處理 query string、環境變數這類「天生都是字串」的資料來源特別實用。 - 建構時驗證:不合法的資料根本建立不出物件,問題資料不會活著進到系統深處。
驗證失敗時會丟出 ValidationError,錯誤訊息精確到欄位:
from pydantic import ValidationError
try:
User(name="Bob", age=-5)
except ValidationError as e:
print(e)
# 1 validation error for User
# age
# Input should be greater than or equal to 0 [type=greater_than_equal, ...]
對 Java 開發者來說,這等於 @Valid + @Min(0) 的效果,但不需要任何 annotation processor 或框架支援,純 Python 就能跑。
JSON 序列化與反序列化
這部分扮演 Jackson 的角色:
# 物件 → JSON 字串
u.model_dump_json()
# '{"name":"Albert","age":35,"email":null}'
# 物件 → dict
u.model_dump()
# {'name': 'Albert', 'age': 35, 'email': None}
# JSON 字串 → 驗證過的物件
raw = '{"name": "Carol", "age": 28}'
user = User.model_validate_json(raw)
# dict → 物件
data = {"name": "Dave", "age": 41}
user = User.model_validate(data)
注意 v2 的命名慣例:方法都以 model_ 開頭(v1 是 .json()、.parse_raw(),升級時要留意)。
巢狀模型
模型可以任意組合,驗證會遞迴進行:
class Address(BaseModel):
city: str
zip_code: str
class Order(BaseModel):
order_id: int
user: User
shipping: Address
items: list[str]
order = Order.model_validate({
"order_id": 1001,
"user": {"name": "Albert", "age": 35},
"shipping": {"city": "Tainan", "zip_code": "700"},
"items": ["keyboard", "mouse"],
})
傳入巢狀 dict,Pydantic 會自動建構出對應的子模型物件——這在解析複雜 API response 時省下大量手動 mapping 的程式碼。
自訂驗證邏輯
內建約束不夠用時,可以掛自訂 validator:
from pydantic import BaseModel, field_validator, model_validator
class Booking(BaseModel):
start: int
end: int
@field_validator("start", "end")
@classmethod
def must_be_positive(cls, v: int) -> int:
if v <= 0:
raise ValueError("必須為正數")
return v
@model_validator(mode="after")
def end_after_start(self):
if self.end <= self.start:
raise ValueError("end 必須大於 start")
return self
field_validator:驗證單一欄位,類似 Java 的自訂ConstraintValidator。model_validator:跨欄位驗證,可以存取整個物件。
常用進階功能速覽
預設值工廠
from pydantic import Field
from datetime import datetime
class Log(BaseModel):
created_at: datetime = Field(default_factory=datetime.now)
tags: list[str] = Field(default_factory=list) # 避免 mutable default 陷阱
嚴格模式
不想要自動轉型(例如 "35" → 35)時:
class StrictUser(BaseModel):
model_config = {"strict": True}
age: int # 傳 "35" 會直接報錯
環境變數設定管理
pydantic-settings 套件可以把 .env / 環境變數直接載入成型別安全的設定物件:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
debug: bool = False
settings = Settings() # 自動讀取環境變數 DATABASE_URL、DEBUG
這比手動 os.getenv() 再轉型乾淨得多,也是 12-factor app 設定管理的常見做法。
為什麼 Pydantic 在 LLM 時代爆紅
Pydantic 模型可以直接輸出 JSON Schema:
User.model_json_schema()
# {
# "title": "User",
# "type": "object",
# "properties": {
# "name": {"type": "string"},
# "age": {"type": "integer", "minimum": 0, "maximum": 150},
# ...
# },
# "required": ["name", "age"]
# }
這一個能力串起了整個現代 Python 生態:
- FastAPI:request/response 模型直接用 Pydantic 宣告,自動產生驗證邏輯 + OpenAPI 文件。
- OpenAI structured outputs / function calling:工具參數的 schema 來自 Pydantic 模型。
- vLLM
guided_json/ Outlines:約束本地 LLM 只能輸出符合 schema 的 JSON,再用同一個模型model_validate_json()解析回來——定義一次,驗證、文件、LLM 輸出約束三件事同時搞定。
一個典型的 LLM 結構化抽取流程:
class Invoice(BaseModel):
vendor: str
amount: float
issue_date: str
schema = Invoice.model_json_schema()
# 把 schema 餵給 LLM 的 guided_json / response_format
llm_output = call_llm(prompt, json_schema=schema)
invoice = Invoice.model_validate_json(llm_output) # 失敗就丟 ValidationError,可重試
LLM 輸出不可靠,但有了 Pydantic 這道閘門,壞輸出會在邊界就被攔下,而不是污染下游邏輯。
與 dataclass 的差異
Python 內建的 @dataclass 也能宣告欄位,但不做任何驗證:
| dataclass | Pydantic BaseModel | |
|---|---|---|
| 型別宣告 | ✅ | ✅ |
| 執行期驗證 | ❌ | ✅ |
| 自動轉型 | ❌ | ✅ |
| JSON 序列化 | 需自己處理 | 內建 |
| JSON Schema | ❌ | 內建 |
經驗法則:純內部資料容器用 dataclass,任何跨越信任邊界的資料(API、檔案、LLM 輸出、使用者輸入)用 Pydantic。
小結
Pydantic 的價值可以濃縮成一句話:讓「資料的形狀」成為程式碼的一部分,而且是會被強制執行的那種。
- 對 Java 開發者:像 Bean Validation + Jackson,但零框架負擔。
- 對 TypeScript 開發者:就是 Python 的 Zod。
- 對所有人:它是 FastAPI 與 LLM structured output 的共同地基,投資報酬率極高。