目錄
02-7. Pydantic v1 vs v2 遷移指南
⏱️ 閱讀時間: 18 分鐘 🎯 難度: ⭐⭐⭐ (進階)
🤔 一句話解釋
Pydantic v2 用 Rust 重寫核心,速度快 5-50 倍,但 API 有重大變更。
🚀 為什麼要遷移到 v2?
效能提升
驗證速度比較(相對於 v1):
┌────────────────────────────────────────────────┐
│ 簡單模型 ████████████████████ 5x 更快 │
│ 巢狀模型 ████████████████████████ 17x 更快│
│ 大型列表 ████████████████████████████ 50x │
└────────────────────────────────────────────────┘主要改進
| 改進項目 | 說明 |
|---|---|
| 效能 | Rust 核心,速度提升 5-50 倍 |
| 嚴格模式 | 支援 strict=True 關閉自動轉型 |
| 更好的錯誤訊息 | 錯誤更詳細、更易讀 |
| Annotated 支援 | 更好的型別提示整合 |
| JSON Schema | 更標準的 JSON Schema 輸出 |
📋 API 變更對照表
模型方法
| v1 方法 | v2 方法 | 說明 |
|---|---|---|
.dict() | .model_dump() | 轉為字典 |
.json() | .model_dump_json() | 轉為 JSON 字串 |
.parse_obj() | .model_validate() | 從字典建立 |
.parse_raw() | .model_validate_json() | 從 JSON 建立 |
.schema() | .model_json_schema() | 取得 JSON Schema |
.construct() | .model_construct() | 不驗證直接建立 |
.copy() | .model_copy() | 複製模型 |
.update_forward_refs() | .model_rebuild() | 更新前向參照 |
設定類別
| v1 | v2 | 說明 |
|---|---|---|
class Config: | model_config = ConfigDict() | 模型設定 |
Config.orm_mode = True | from_attributes=True | ORM 模式 |
Config.allow_mutation = False | frozen=True | 不可變模型 |
Config.extra = "forbid" | extra="forbid" | 禁止額外欄位 |
驗證器
| v1 | v2 | 說明 |
|---|---|---|
@validator | @field_validator | 欄位驗證器 |
@root_validator | @model_validator | 模型驗證器 |
pre=True | mode='before' | 前置驗證 |
always=True | 預設行為 | 總是執行 |
🔄 遷移範例
1. 基本模型定義
v1:
from pydantic import BaseModel
class User(BaseModel):
name: str
email: str
class Config:
orm_mode = True
extra = "forbid"v2:
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
name: str
email: str
model_config = ConfigDict(
from_attributes=True,
extra="forbid"
)2. 序列化方法
v1:
user = User(name="John", email="john@example.com")
# 轉為字典
data = user.dict()
data = user.dict(exclude={"email"})
data = user.dict(exclude_unset=True)
# 轉為 JSON
json_str = user.json()
json_str = user.json(indent=2)v2:
user = User(name="John", email="john@example.com")
# 轉為字典
data = user.model_dump()
data = user.model_dump(exclude={"email"})
data = user.model_dump(exclude_unset=True)
# 轉為 JSON
json_str = user.model_dump_json()
json_str = user.model_dump_json(indent=2)3. 反序列化方法
v1:
from pydantic import BaseModel
class User(BaseModel):
name: str
email: str
# 從字典建立
data = {"name": "John", "email": "john@example.com"}
user = User.parse_obj(data)
# 從 JSON 建立
json_str = '{"name": "John", "email": "john@example.com"}'
user = User.parse_raw(json_str)
# 從 ORM 物件建立
user = User.from_orm(orm_user)v2:
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
name: str
email: str
model_config = ConfigDict(from_attributes=True)
# 從字典建立
data = {"name": "John", "email": "john@example.com"}
user = User.model_validate(data)
# 從 JSON 建立
json_str = '{"name": "John", "email": "john@example.com"}'
user = User.model_validate_json(json_str)
# 從 ORM 物件建立
user = User.model_validate(orm_user)4. 欄位驗證器
v1:
from pydantic import BaseModel, validator
class User(BaseModel):
name: str
email: str
@validator('name')
def name_not_empty(cls, v):
if not v.strip():
raise ValueError('name cannot be empty')
return v.strip()
@validator('email', pre=True)
def normalize_email(cls, v):
return v.lower().strip()
@validator('*') # 所有欄位
def strip_strings(cls, v):
if isinstance(v, str):
return v.strip()
return vv2:
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
email: str
@field_validator('name')
@classmethod
def name_not_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError('name cannot be empty')
return v.strip()
@field_validator('email', mode='before')
@classmethod
def normalize_email(cls, v: str) -> str:
return v.lower().strip()
@field_validator('*') # 所有欄位
@classmethod
def strip_strings(cls, v):
if isinstance(v, str):
return v.strip()
return v5. 模型驗證器(root_validator)
v1:
from pydantic import BaseModel, root_validator
class DateRange(BaseModel):
start_date: date
end_date: date
@root_validator
def check_dates(cls, values):
start = values.get('start_date')
end = values.get('end_date')
if start and end and start > end:
raise ValueError('start_date must be before end_date')
return values
@root_validator(pre=True)
def preprocess(cls, values):
# 前置處理
return valuesv2:
from pydantic import BaseModel, model_validator
class DateRange(BaseModel):
start_date: date
end_date: date
@model_validator(mode='after')
def check_dates(self) -> 'DateRange':
if self.start_date > self.end_date:
raise ValueError('start_date must be before end_date')
return self
@model_validator(mode='before')
@classmethod
def preprocess(cls, data: dict) -> dict:
# 前置處理
return data6. Field 參數
v1:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0)
# regex 變成 pattern
sku: str = Field(..., regex=r'^[A-Z]{3}-\d{4}$')
# const 已移除
version: str = Field("1.0", const=True)v2:
from pydantic import BaseModel, Field
from typing import Literal
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0)
# regex 改名為 pattern
sku: str = Field(..., pattern=r'^[A-Z]{3}-\d{4}$')
# const 用 Literal 取代
version: Literal["1.0"] = "1.0"7. Optional 和 None 的處理
v1:
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
# v1: Optional[str] 自動預設為 None
nickname: Optional[str] # 預設 Nonev2:
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
# v2: 必須明確指定預設值
nickname: Optional[str] = None # 必須明確寫 = None
# 或者用這種方式表示必填但可以是 None
nickname: str | None # 必填,但可以傳 None8. 自訂型別
v1:
from pydantic import BaseModel
class PositiveInt(int):
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
if not isinstance(v, int) or v <= 0:
raise ValueError('must be positive integer')
return cls(v)
class Item(BaseModel):
quantity: PositiveIntv2:
from pydantic import BaseModel, BeforeValidator, AfterValidator
from typing import Annotated
def validate_positive(v: int) -> int:
if v <= 0:
raise ValueError('must be positive integer')
return v
PositiveInt = Annotated[int, AfterValidator(validate_positive)]
class Item(BaseModel):
quantity: PositiveInt9. JSON Schema
v1:
from pydantic import BaseModel
class User(BaseModel):
name: str
email: str
# 取得 JSON Schema
schema = User.schema()
schema_json = User.schema_json(indent=2)v2:
from pydantic import BaseModel
import json
class User(BaseModel):
name: str
email: str
# 取得 JSON Schema
schema = User.model_json_schema()
schema_json = json.dumps(schema, indent=2)🛠️ 漸進式遷移策略
使用相容層
Pydantic v2 提供了向後相容的方法(但會有棄用警告):
from pydantic import BaseModel
import warnings
# 暫時忽略棄用警告
warnings.filterwarnings("ignore", category=DeprecationWarning)
class User(BaseModel):
name: str
user = User(name="John")
# 這些 v1 方法在 v2 中仍可使用,但會有警告
user.dict() # 棄用,改用 model_dump()
user.json() # 棄用,改用 model_dump_json()
User.parse_obj() # 棄用,改用 model_validate()分階段遷移
# 階段 1: 更新 Config 為 model_config
class User(BaseModel):
name: str
# 舊的方式
# class Config:
# orm_mode = True
# 新的方式
model_config = ConfigDict(from_attributes=True)
# 階段 2: 更新序列化方法
# 舊: user.dict()
# 新: user.model_dump()
# 階段 3: 更新驗證器
# 舊: @validator
# 新: @field_validator
# 階段 4: 更新自訂型別
# 舊: __get_validators__
# 新: Annotated + validators⚠️ 破壞性變更注意事項
1. 嚴格模式變更
from pydantic import BaseModel
class User(BaseModel):
age: int
# v1: 字串 "25" 會自動轉為 int 25
# v2: 預設行為相同,但可以用 strict=True 關閉
user = User.model_validate({"age": "25"}) # ✅ age=25
# 嚴格模式
user = User.model_validate({"age": "25"}, strict=True) # ❌ ValidationError2. 空字串處理
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
name: str
nickname: Optional[str] = None
# v1: 空字串保持為空字串
# v2: 空字串保持為空字串(這點沒變)
# 但如果你想讓空字串變成 None,需要自己處理
from pydantic import field_validator
class User(BaseModel):
name: str
nickname: Optional[str] = None
@field_validator('nickname', mode='before')
@classmethod
def empty_to_none(cls, v):
if v == '':
return None
return v3. 預設值工廠
from pydantic import BaseModel, Field
from typing import List
# v1 和 v2 都支援
class User(BaseModel):
tags: List[str] = Field(default_factory=list)
# v2 新增的簡潔語法
class User(BaseModel):
tags: List[str] = [] # v2 會自動複製,不會共享參照4. 欄位排序
# v2 中欄位順序可能影響驗證(依賴其他欄位時)
from pydantic import BaseModel, model_validator
class User(BaseModel):
password: str
confirm_password: str
@model_validator(mode='after')
def check_passwords(self):
if self.password != self.confirm_password:
raise ValueError('passwords do not match')
return self📝 完整遷移範例
v1 版本
from pydantic import BaseModel, Field, validator, root_validator
from typing import Optional, List
from datetime import datetime
class Address(BaseModel):
street: str
city: str
country: str = "Taiwan"
class Config:
extra = "forbid"
class UserV1(BaseModel):
id: int
username: str = Field(..., min_length=3, max_length=50)
email: str = Field(..., regex=r'^[\w\.-]+@[\w\.-]+\.\w+$')
password: str = Field(..., min_length=8)
age: Optional[int] = Field(None, ge=0, le=150)
tags: List[str] = []
address: Optional[Address] = None
created_at: datetime = Field(default_factory=datetime.now)
class Config:
orm_mode = True
extra = "forbid"
validate_assignment = True
@validator('username')
def username_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('must be alphanumeric')
return v.lower()
@validator('email')
def normalize_email(cls, v):
return v.lower().strip()
@validator('tags', pre=True, always=True)
def default_tags(cls, v):
return v or []
@root_validator
def check_age_for_tags(cls, values):
age = values.get('age')
tags = values.get('tags', [])
if age and age < 18 and 'adult' in tags:
raise ValueError('underage users cannot have adult tag')
return valuesv2 版本
from pydantic import (
BaseModel,
Field,
field_validator,
model_validator,
ConfigDict
)
from typing import Optional, List
from datetime import datetime
class Address(BaseModel):
street: str
city: str
country: str = "Taiwan"
model_config = ConfigDict(extra="forbid")
class UserV2(BaseModel):
id: int
username: str = Field(..., min_length=3, max_length=50)
email: str = Field(..., pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
password: str = Field(..., min_length=8)
age: Optional[int] = Field(None, ge=0, le=150)
tags: List[str] = Field(default_factory=list)
address: Optional[Address] = None
created_at: datetime = Field(default_factory=datetime.now)
model_config = ConfigDict(
from_attributes=True,
extra="forbid",
validate_assignment=True
)
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('must be alphanumeric')
return v.lower()
@field_validator('email')
@classmethod
def normalize_email(cls, v: str) -> str:
return v.lower().strip()
@field_validator('tags', mode='before')
@classmethod
def default_tags(cls, v) -> List[str]:
return v or []
@model_validator(mode='after')
def check_age_for_tags(self) -> 'UserV2':
if self.age and self.age < 18 and 'adult' in self.tags:
raise ValueError('underage users cannot have adult tag')
return self使用方式比較
# ===== 建立實例 =====
# v1 和 v2 相同
user = UserV2(
id=1,
username="john123",
email="John@Example.com",
password="secret123",
age=25
)
# ===== 序列化 =====
# v1
# data = user.dict()
# json_str = user.json()
# v2
data = user.model_dump()
json_str = user.model_dump_json()
# ===== 反序列化 =====
# v1
# user = UserV1.parse_obj(data)
# user = UserV1.parse_raw(json_str)
# v2
user = UserV2.model_validate(data)
user = UserV2.model_validate_json(json_str)
# ===== 從 ORM 建立 =====
# v1
# user = UserV1.from_orm(orm_user)
# v2
user = UserV2.model_validate(orm_user)
# ===== 取得 Schema =====
# v1
# schema = UserV1.schema()
# v2
schema = UserV2.model_json_schema()🔧 自動化遷移工具
使用 bump-pydantic
# 安裝
pip install bump-pydantic
# 執行遷移
bump-pydantic --diff . # 預覽變更
bump-pydantic . # 執行遷移手動搜尋替換
# 搜尋需要更新的 v1 API
grep -r "\.dict(" .
grep -r "\.json(" .
grep -r "parse_obj" .
grep -r "parse_raw" .
grep -r "@validator" .
grep -r "@root_validator" .
grep -r "class Config:" .
grep -r "orm_mode" .
grep -r "regex=" .✅ 遷移檢查清單
安裝 Pydantic v2
pip install "pydantic>=2.0"更新 Config 為 model_config
class Config:→model_config = ConfigDict()orm_mode = True→from_attributes=True
更新序列化方法
.dict()→.model_dump().json()→.model_dump_json()
更新反序列化方法
.parse_obj()→.model_validate().parse_raw()→.model_validate_json().from_orm()→.model_validate()
更新驗證器
@validator→@field_validator+@classmethod@root_validator→@model_validatorpre=True→mode='before'
更新 Field 參數
regex=→pattern=const=True→ 使用Literal
更新 Optional 處理
Optional[str]→Optional[str] = None
執行測試
pytest移除棄用警告
python -W error::DeprecationWarning your_script.py
✅ 重點總結
主要變更
| 類別 | v1 | v2 |
|---|---|---|
| 設定 | class Config | model_config = ConfigDict() |
| 序列化 | .dict(), .json() | .model_dump(), .model_dump_json() |
| 反序列化 | .parse_obj(), .parse_raw() | .model_validate(), .model_validate_json() |
| 驗證器 | @validator, @root_validator | @field_validator, @model_validator |
| 正規表達式 | regex= | pattern= |
| ORM 模式 | orm_mode=True | from_attributes=True |
遷移建議
- 使用 bump-pydantic 工具自動處理大部分變更
- 分階段遷移,先更新 Config,再更新方法
- 執行測試確保功能正常
- 檢查棄用警告找出遺漏的地方
🎤 面試這樣答
Q: Pydantic v1 和 v2 的主要差異是什麼?
答案:
- 效能:v2 用 Rust 重寫核心,速度快 5-50 倍
- API 變更:
.dict()→.model_dump()@validator→@field_validatorclass Config→model_config = ConfigDict()- 驗證器語法:v2 的驗證器需要
@classmethod裝飾器- 嚴格模式:v2 支援
strict=True關閉自動型別轉換- 更好的型別支援:更好的 Annotated 整合
🤓 小測驗
v1 的
.dict()在 v2 中要用什麼方法?v2 中如何啟用 ORM 模式?
v1 的
@root_validator在 v2 中對應什麼?
上一篇: 02-6. Settings 管理與環境變數 下一篇: 03-1. SQLAlchemy 基礎
最後更新:2025-12-17