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()更新前向參照

設定類別

v1v2說明
class Config:model_config = ConfigDict()模型設定
Config.orm_mode = Truefrom_attributes=TrueORM 模式
Config.allow_mutation = Falsefrozen=True不可變模型
Config.extra = "forbid"extra="forbid"禁止額外欄位

驗證器

v1v2說明
@validator@field_validator欄位驗證器
@root_validator@model_validator模型驗證器
pre=Truemode='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 v

v2:

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 v

5. 模型驗證器(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 values

v2:

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 data

6. 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]  # 預設 None

v2:

from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    # v2: 必須明確指定預設值
    nickname: Optional[str] = None  # 必須明確寫 = None

    # 或者用這種方式表示必填但可以是 None
    nickname: str | None  # 必填,但可以傳 None

8. 自訂型別

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: PositiveInt

v2:

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: PositiveInt

9. 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)  # ❌ ValidationError

2. 空字串處理

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 v

3. 預設值工廠

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 values

v2 版本

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 = Truefrom_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_validator
    • pre=Truemode='before'
  • 更新 Field 參數

    • regex=pattern=
    • const=True → 使用 Literal
  • 更新 Optional 處理

    • Optional[str]Optional[str] = None
  • 執行測試

    pytest
  • 移除棄用警告

    python -W error::DeprecationWarning your_script.py

✅ 重點總結

主要變更

類別v1v2
設定class Configmodel_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=Truefrom_attributes=True

遷移建議

  1. 使用 bump-pydantic 工具自動處理大部分變更
  2. 分階段遷移,先更新 Config,再更新方法
  3. 執行測試確保功能正常
  4. 檢查棄用警告找出遺漏的地方

🎤 面試這樣答

Q: Pydantic v1 和 v2 的主要差異是什麼?

答案:

  1. 效能:v2 用 Rust 重寫核心,速度快 5-50 倍
  2. API 變更
    • .dict().model_dump()
    • @validator@field_validator
    • class Configmodel_config = ConfigDict()
  3. 驗證器語法:v2 的驗證器需要 @classmethod 裝飾器
  4. 嚴格模式:v2 支援 strict=True 關閉自動型別轉換
  5. 更好的型別支援:更好的 Annotated 整合

🤓 小測驗

  1. v1 的 .dict() 在 v2 中要用什麼方法?

  2. v2 中如何啟用 ORM 模式?

  3. v1 的 @root_validator 在 v2 中對應什麼?


上一篇: 02-6. Settings 管理與環境變數 下一篇: 03-1. SQLAlchemy 基礎


最後更新:2025-12-17

0%