# 

# 02-7. Pydantic v1 vs v2 遷移指南

&gt; ⏱️ **閱讀時間：** 18 分鐘
&gt; 🎯 **難度：** ⭐⭐⭐ (進階)

---

## 🤔 一句話解釋

**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 = &#34;forbid&#34;` | `extra=&#34;forbid&#34;` | 禁止額外欄位 |

### 驗證器

| v1 | v2 | 說明 |
|----|----|----|
| `@validator` | `@field_validator` | 欄位驗證器 |
| `@root_validator` | `@model_validator` | 模型驗證器 |
| `pre=True` | `mode=&#39;before&#39;` | 前置驗證 |
| `always=True` | 預設行為 | 總是執行 |

---

## 🔄 遷移範例

### 1. 基本模型定義

**v1：**

```python
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str

    class Config:
        orm_mode = True
        extra = &#34;forbid&#34;
```

**v2：**

```python
from pydantic import BaseModel, ConfigDict

class User(BaseModel):
    name: str
    email: str

    model_config = ConfigDict(
        from_attributes=True,
        extra=&#34;forbid&#34;
    )
```

### 2. 序列化方法

**v1：**

```python
user = User(name=&#34;John&#34;, email=&#34;john@example.com&#34;)

# 轉為字典
data = user.dict()
data = user.dict(exclude={&#34;email&#34;})
data = user.dict(exclude_unset=True)

# 轉為 JSON
json_str = user.json()
json_str = user.json(indent=2)
```

**v2：**

```python
user = User(name=&#34;John&#34;, email=&#34;john@example.com&#34;)

# 轉為字典
data = user.model_dump()
data = user.model_dump(exclude={&#34;email&#34;})
data = user.model_dump(exclude_unset=True)

# 轉為 JSON
json_str = user.model_dump_json()
json_str = user.model_dump_json(indent=2)
```

### 3. 反序列化方法

**v1：**

```python
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str

# 從字典建立
data = {&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;john@example.com&#34;}
user = User.parse_obj(data)

# 從 JSON 建立
json_str = &#39;{&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;john@example.com&#34;}&#39;
user = User.parse_raw(json_str)

# 從 ORM 物件建立
user = User.from_orm(orm_user)
```

**v2：**

```python
from pydantic import BaseModel, ConfigDict

class User(BaseModel):
    name: str
    email: str

    model_config = ConfigDict(from_attributes=True)

# 從字典建立
data = {&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;john@example.com&#34;}
user = User.model_validate(data)

# 從 JSON 建立
json_str = &#39;{&#34;name&#34;: &#34;John&#34;, &#34;email&#34;: &#34;john@example.com&#34;}&#39;
user = User.model_validate_json(json_str)

# 從 ORM 物件建立
user = User.model_validate(orm_user)
```

### 4. 欄位驗證器

**v1：**

```python
from pydantic import BaseModel, validator

class User(BaseModel):
    name: str
    email: str

    @validator(&#39;name&#39;)
    def name_not_empty(cls, v):
        if not v.strip():
            raise ValueError(&#39;name cannot be empty&#39;)
        return v.strip()

    @validator(&#39;email&#39;, pre=True)
    def normalize_email(cls, v):
        return v.lower().strip()

    @validator(&#39;*&#39;)  # 所有欄位
    def strip_strings(cls, v):
        if isinstance(v, str):
            return v.strip()
        return v
```

**v2：**

```python
from pydantic import BaseModel, field_validator

class User(BaseModel):
    name: str
    email: str

    @field_validator(&#39;name&#39;)
    @classmethod
    def name_not_empty(cls, v: str) -&gt; str:
        if not v.strip():
            raise ValueError(&#39;name cannot be empty&#39;)
        return v.strip()

    @field_validator(&#39;email&#39;, mode=&#39;before&#39;)
    @classmethod
    def normalize_email(cls, v: str) -&gt; str:
        return v.lower().strip()

    @field_validator(&#39;*&#39;)  # 所有欄位
    @classmethod
    def strip_strings(cls, v):
        if isinstance(v, str):
            return v.strip()
        return v
```

### 5. 模型驗證器（root_validator）

**v1：**

```python
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(&#39;start_date&#39;)
        end = values.get(&#39;end_date&#39;)
        if start and end and start &gt; end:
            raise ValueError(&#39;start_date must be before end_date&#39;)
        return values

    @root_validator(pre=True)
    def preprocess(cls, values):
        # 前置處理
        return values
```

**v2：**

```python
from pydantic import BaseModel, model_validator

class DateRange(BaseModel):
    start_date: date
    end_date: date

    @model_validator(mode=&#39;after&#39;)
    def check_dates(self) -&gt; &#39;DateRange&#39;:
        if self.start_date &gt; self.end_date:
            raise ValueError(&#39;start_date must be before end_date&#39;)
        return self

    @model_validator(mode=&#39;before&#39;)
    @classmethod
    def preprocess(cls, data: dict) -&gt; dict:
        # 前置處理
        return data
```

### 6. Field 參數

**v1：**

```python
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&#39;^[A-Z]{3}-\d{4}$&#39;)

    # const 已移除
    version: str = Field(&#34;1.0&#34;, const=True)
```

**v2：**

```python
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&#39;^[A-Z]{3}-\d{4}$&#39;)

    # const 用 Literal 取代
    version: Literal[&#34;1.0&#34;] = &#34;1.0&#34;
```

### 7. Optional 和 None 的處理

**v1：**

```python
from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    # v1: Optional[str] 自動預設為 None
    nickname: Optional[str]  # 預設 None
```

**v2：**

```python
from pydantic import BaseModel
from typing import Optional

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

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

### 8. 自訂型別

**v1：**

```python
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 &lt;= 0:
            raise ValueError(&#39;must be positive integer&#39;)
        return cls(v)

class Item(BaseModel):
    quantity: PositiveInt
```

**v2：**

```python
from pydantic import BaseModel, BeforeValidator, AfterValidator
from typing import Annotated

def validate_positive(v: int) -&gt; int:
    if v &lt;= 0:
        raise ValueError(&#39;must be positive integer&#39;)
    return v

PositiveInt = Annotated[int, AfterValidator(validate_positive)]

class Item(BaseModel):
    quantity: PositiveInt
```

### 9. JSON Schema

**v1：**

```python
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str

# 取得 JSON Schema
schema = User.schema()
schema_json = User.schema_json(indent=2)
```

**v2：**

```python
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 提供了向後相容的方法（但會有棄用警告）：

```python
from pydantic import BaseModel
import warnings

# 暫時忽略棄用警告
warnings.filterwarnings(&#34;ignore&#34;, category=DeprecationWarning)

class User(BaseModel):
    name: str

user = User(name=&#34;John&#34;)

# 這些 v1 方法在 v2 中仍可使用，但會有警告
user.dict()      # 棄用，改用 model_dump()
user.json()      # 棄用，改用 model_dump_json()
User.parse_obj() # 棄用，改用 model_validate()
```

### 分階段遷移

```python
# 階段 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 &#43; validators
```

---

## ⚠️ 破壞性變更注意事項

### 1. 嚴格模式變更

```python
from pydantic import BaseModel

class User(BaseModel):
    age: int

# v1: 字串 &#34;25&#34; 會自動轉為 int 25
# v2: 預設行為相同，但可以用 strict=True 關閉

user = User.model_validate({&#34;age&#34;: &#34;25&#34;})  # ✅ age=25

# 嚴格模式
user = User.model_validate({&#34;age&#34;: &#34;25&#34;}, strict=True)  # ❌ ValidationError
```

### 2. 空字串處理

```python
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(&#39;nickname&#39;, mode=&#39;before&#39;)
    @classmethod
    def empty_to_none(cls, v):
        if v == &#39;&#39;:
            return None
        return v
```

### 3. 預設值工廠

```python
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. 欄位排序

```python
# v2 中欄位順序可能影響驗證（依賴其他欄位時）
from pydantic import BaseModel, model_validator

class User(BaseModel):
    password: str
    confirm_password: str

    @model_validator(mode=&#39;after&#39;)
    def check_passwords(self):
        if self.password != self.confirm_password:
            raise ValueError(&#39;passwords do not match&#39;)
        return self
```

---

## 📝 完整遷移範例

### v1 版本

```python
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 = &#34;Taiwan&#34;

    class Config:
        extra = &#34;forbid&#34;

class UserV1(BaseModel):
    id: int
    username: str = Field(..., min_length=3, max_length=50)
    email: str = Field(..., regex=r&#39;^[\w\.-]&#43;@[\w\.-]&#43;\.\w&#43;$&#39;)
    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 = &#34;forbid&#34;
        validate_assignment = True

    @validator(&#39;username&#39;)
    def username_alphanumeric(cls, v):
        if not v.isalnum():
            raise ValueError(&#39;must be alphanumeric&#39;)
        return v.lower()

    @validator(&#39;email&#39;)
    def normalize_email(cls, v):
        return v.lower().strip()

    @validator(&#39;tags&#39;, pre=True, always=True)
    def default_tags(cls, v):
        return v or []

    @root_validator
    def check_age_for_tags(cls, values):
        age = values.get(&#39;age&#39;)
        tags = values.get(&#39;tags&#39;, [])
        if age and age &lt; 18 and &#39;adult&#39; in tags:
            raise ValueError(&#39;underage users cannot have adult tag&#39;)
        return values
```

### v2 版本

```python
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 = &#34;Taiwan&#34;

    model_config = ConfigDict(extra=&#34;forbid&#34;)

class UserV2(BaseModel):
    id: int
    username: str = Field(..., min_length=3, max_length=50)
    email: str = Field(..., pattern=r&#39;^[\w\.-]&#43;@[\w\.-]&#43;\.\w&#43;$&#39;)
    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=&#34;forbid&#34;,
        validate_assignment=True
    )

    @field_validator(&#39;username&#39;)
    @classmethod
    def username_alphanumeric(cls, v: str) -&gt; str:
        if not v.isalnum():
            raise ValueError(&#39;must be alphanumeric&#39;)
        return v.lower()

    @field_validator(&#39;email&#39;)
    @classmethod
    def normalize_email(cls, v: str) -&gt; str:
        return v.lower().strip()

    @field_validator(&#39;tags&#39;, mode=&#39;before&#39;)
    @classmethod
    def default_tags(cls, v) -&gt; List[str]:
        return v or []

    @model_validator(mode=&#39;after&#39;)
    def check_age_for_tags(self) -&gt; &#39;UserV2&#39;:
        if self.age and self.age &lt; 18 and &#39;adult&#39; in self.tags:
            raise ValueError(&#39;underage users cannot have adult tag&#39;)
        return self
```

### 使用方式比較

```python
# ===== 建立實例 =====
# v1 和 v2 相同
user = UserV2(
    id=1,
    username=&#34;john123&#34;,
    email=&#34;John@Example.com&#34;,
    password=&#34;secret123&#34;,
    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

```bash
# 安裝
pip install bump-pydantic

# 執行遷移
bump-pydantic --diff .          # 預覽變更
bump-pydantic .                  # 執行遷移
```

### 手動搜尋替換

```bash
# 搜尋需要更新的 v1 API
grep -r &#34;\.dict(&#34; .
grep -r &#34;\.json(&#34; .
grep -r &#34;parse_obj&#34; .
grep -r &#34;parse_raw&#34; .
grep -r &#34;@validator&#34; .
grep -r &#34;@root_validator&#34; .
grep -r &#34;class Config:&#34; .
grep -r &#34;orm_mode&#34; .
grep -r &#34;regex=&#34; .
```

---

## ✅ 遷移檢查清單

- [ ] **安裝 Pydantic v2**
  ```bash
  pip install &#34;pydantic&gt;=2.0&#34;
  ```

- [ ] **更新 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` &#43; `@classmethod`
  - `@root_validator` → `@model_validator`
  - `pre=True` → `mode=&#39;before&#39;`

- [ ] **更新 Field 參數**
  - `regex=` → `pattern=`
  - `const=True` → 使用 `Literal`

- [ ] **更新 Optional 處理**
  - `Optional[str]` → `Optional[str] = None`

- [ ] **執行測試**
  ```bash
  pytest
  ```

- [ ] **移除棄用警告**
  ```bash
  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` |

### 遷移建議

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

---

## 🎤 面試這樣答

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

**答案：**

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

---

## 🤓 小測驗

1. v1 的 `.dict()` 在 v2 中要用什麼方法？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   `.model_dump()`
   &lt;/details&gt;

2. v2 中如何啟用 ORM 模式？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   `model_config = ConfigDict(from_attributes=True)`
   &lt;/details&gt;

3. v1 的 `@root_validator` 在 v2 中對應什麼？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   `@model_validator(mode=&#39;after&#39;)` 或 `@model_validator(mode=&#39;before&#39;)`
   &lt;/details&gt;

---

**上一篇：** [02-6. Settings 管理與環境變數](./02-6)
**下一篇：** [03-1. SQLAlchemy 基礎](./03-1)

---

最後更新：2025-12-17


---

> 作者: luk  
> URL: https://yoru-karu-blog-lalaluk-52581ac5e0cef170a3c8922c19182ecb6f7bd604.gitlab.io/posts/tutorial/fastapi/02-7/  

