# 

# 02-2. 欄位驗證與約束

&gt; ⏱️ **閱讀時間：** 15 分鐘
&gt; 🎯 **難度：** ⭐⭐ (基礎)

---

## 🤔 一句話解釋

**Pydantic 提供豐富的內建驗證器，讓你用簡單的參數就能定義複雜的驗證規則。**

---

## 🔢 數字驗證

### 基本約束

```python
from pydantic import BaseModel, Field

class NumberConstraints(BaseModel):
    # 大於 (greater than)
    gt_example: int = Field(gt=0)       # &gt; 0

    # 大於等於 (greater than or equal)
    ge_example: int = Field(ge=0)       # &gt;= 0

    # 小於 (less than)
    lt_example: int = Field(lt=100)     # &lt; 100

    # 小於等於 (less than or equal)
    le_example: int = Field(le=100)     # &lt;= 100

    # 組合使用
    age: int = Field(ge=0, le=150)      # 0 &lt;= age &lt;= 150
    price: float = Field(gt=0, le=99999.99)

    # 倍數
    quantity: int = Field(multiple_of=5)  # 必須是 5 的倍數

# 測試
NumberConstraints(
    gt_example=1,
    ge_example=0,
    lt_example=99,
    le_example=100,
    age=25,
    price=99.99,
    quantity=15
)
```

### 實際應用

```python
from pydantic import BaseModel, Field
from decimal import Decimal

class PaymentRequest(BaseModel):
    &#34;&#34;&#34;付款請求&#34;&#34;&#34;
    amount: Decimal = Field(
        ...,
        gt=0,
        le=Decimal(&#34;1000000.00&#34;),
        decimal_places=2,
        description=&#34;付款金額，最多 100 萬&#34;
    )

    installments: int = Field(
        default=1,
        ge=1,
        le=24,
        description=&#34;分期期數，1-24 期&#34;
    )

class DiscountCoupon(BaseModel):
    &#34;&#34;&#34;折扣券&#34;&#34;&#34;
    discount_percent: int = Field(
        ...,
        ge=1,
        le=100,
        description=&#34;折扣百分比，1-100&#34;
    )

    min_order_amount: Decimal = Field(
        default=Decimal(&#34;0&#34;),
        ge=0,
        description=&#34;最低訂單金額&#34;
    )

    max_discount: Decimal = Field(
        default=Decimal(&#34;1000&#34;),
        gt=0,
        description=&#34;最高折抵金額&#34;
    )
```

---

## 📝 字串驗證

### 長度約束

```python
from pydantic import BaseModel, Field

class StringConstraints(BaseModel):
    # 最小長度
    username: str = Field(min_length=3)

    # 最大長度
    bio: str = Field(max_length=500)

    # 組合
    password: str = Field(min_length=8, max_length=128)

    # 固定長度（使用正規表達式）
    country_code: str = Field(pattern=r&#34;^[A-Z]{2}$&#34;)  # 兩個大寫字母
```

### 正規表達式驗證

```python
from pydantic import BaseModel, Field

class PatternValidation(BaseModel):
    # 電話號碼（台灣手機）
    phone: str = Field(
        pattern=r&#34;^09\d{8}$&#34;,
        description=&#34;台灣手機號碼&#34;
    )

    # 身分證字號
    taiwan_id: str = Field(
        pattern=r&#34;^[A-Z][12]\d{8}$&#34;,
        description=&#34;台灣身分證字號&#34;
    )

    # 郵遞區號
    zip_code: str = Field(
        pattern=r&#34;^\d{3,5}$&#34;,
        description=&#34;郵遞區號&#34;
    )

    # Slug（URL 友善字串）
    slug: str = Field(
        pattern=r&#34;^[a-z0-9]&#43;(?:-[a-z0-9]&#43;)*$&#34;,
        description=&#34;URL slug&#34;
    )

    # 密碼（至少一個大寫、一個小寫、一個數字）
    strong_password: str = Field(
        pattern=r&#34;^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$&#34;,
        description=&#34;強密碼&#34;
    )
```

### 字串處理

```python
from pydantic import BaseModel, Field
from typing import Annotated
from pydantic.functional_validators import AfterValidator

def normalize_email(v: str) -&gt; str:
    return v.lower().strip()

def strip_whitespace(v: str) -&gt; str:
    return v.strip()

class UserInput(BaseModel):
    # 使用 Annotated 添加處理
    email: Annotated[str, AfterValidator(normalize_email)]
    name: Annotated[str, AfterValidator(strip_whitespace)]

# 測試
user = UserInput(email=&#34;  JOHN@EXAMPLE.COM  &#34;, name=&#34;  John Doe  &#34;)
print(user.email)  # john@example.com
print(user.name)   # John Doe
```

---

## 📋 列表與集合驗證

### 長度約束

```python
from pydantic import BaseModel, Field
from typing import List, Set

class CollectionConstraints(BaseModel):
    # 列表最小/最大長度
    tags: List[str] = Field(
        default_factory=list,
        min_length=0,
        max_length=10,
        description=&#34;標籤，最多 10 個&#34;
    )

    # 集合
    categories: Set[str] = Field(
        default_factory=set,
        max_length=5,
        description=&#34;分類，最多 5 個&#34;
    )

    # 非空列表
    items: List[int] = Field(
        ...,
        min_length=1,
        description=&#34;至少要有一個項目&#34;
    )
```

### 列表元素驗證

```python
from pydantic import BaseModel, Field
from typing import List
from typing_extensions import Annotated

class Order(BaseModel):
    # 列表中的每個元素都會被驗證
    quantities: List[int] = Field(
        ...,
        description=&#34;每個項目的數量&#34;
    )

    # 使用 Annotated 對元素進行約束
    prices: List[Annotated[float, Field(gt=0)]] = Field(
        default_factory=list,
        description=&#34;價格列表，每個價格必須大於 0&#34;
    )
```

---

## 📧 內建特殊型別

### Email 驗證

```python
from pydantic import BaseModel, EmailStr

class User(BaseModel):
    email: EmailStr  # 自動驗證 email 格式

# 測試
User(email=&#34;john@example.com&#34;)     # ✅
User(email=&#34;invalid-email&#34;)        # ❌ ValidationError
```

### URL 驗證

```python
from pydantic import BaseModel, HttpUrl, AnyUrl

class Links(BaseModel):
    # HTTP/HTTPS URL
    website: HttpUrl

    # 任意 URL（包括 ftp, file 等）
    resource: AnyUrl

# 測試
Links(
    website=&#34;https://example.com&#34;,
    resource=&#34;ftp://files.example.com/data&#34;
)
```

### 其他特殊型別

```python
from pydantic import (
    BaseModel,
    EmailStr,
    HttpUrl,
    IPvAnyAddress,
    SecretStr,
    Json,
    UUID4,
)
from typing import List

class SpecialTypes(BaseModel):
    # IP 地址
    ip_address: IPvAnyAddress

    # 密碼（不會在日誌中顯示）
    password: SecretStr

    # UUID v4
    user_id: UUID4

    # JSON 字串會被解析
    metadata: Json[dict]

# 使用 SecretStr
user = SpecialTypes(
    ip_address=&#34;192.168.1.1&#34;,
    password=&#34;secret123&#34;,
    user_id=&#34;550e8400-e29b-41d4-a716-446655440000&#34;,
    metadata=&#39;{&#34;key&#34;: &#34;value&#34;}&#39;
)

print(user.password)                    # SecretStr(&#39;**********&#39;)
print(user.password.get_secret_value()) # secret123
```

---

## 🎛️ 條件約束

### 使用 Literal 限制值

```python
from pydantic import BaseModel
from typing import Literal

class Config(BaseModel):
    environment: Literal[&#34;development&#34;, &#34;staging&#34;, &#34;production&#34;]
    log_level: Literal[&#34;DEBUG&#34;, &#34;INFO&#34;, &#34;WARNING&#34;, &#34;ERROR&#34;]

# 測試
Config(environment=&#34;production&#34;, log_level=&#34;INFO&#34;)  # ✅
Config(environment=&#34;invalid&#34;, log_level=&#34;INFO&#34;)     # ❌
```

### 使用 Enum

```python
from pydantic import BaseModel
from enum import Enum

class Status(str, Enum):
    PENDING = &#34;pending&#34;
    APPROVED = &#34;approved&#34;
    REJECTED = &#34;rejected&#34;

class OrderStatus(str, Enum):
    CREATED = &#34;created&#34;
    PAID = &#34;paid&#34;
    SHIPPED = &#34;shipped&#34;
    DELIVERED = &#34;delivered&#34;
    CANCELLED = &#34;cancelled&#34;

class Order(BaseModel):
    id: int
    status: OrderStatus = OrderStatus.CREATED

# 使用
order = Order(id=1, status=&#34;paid&#34;)  # 字串會轉成 Enum
print(order.status)                  # OrderStatus.PAID
print(order.status.value)            # paid
```

### 聯合型別

```python
from pydantic import BaseModel
from typing import Union, List

class StringOrList(BaseModel):
    # 可以是字串或字串列表
    tags: Union[str, List[str]]

# 兩種都可以
StringOrList(tags=&#34;python&#34;)
StringOrList(tags=[&#34;python&#34;, &#34;fastapi&#34;])

# Python 3.10&#43; 語法
class StringOrList2(BaseModel):
    tags: str | list[str]
```

---

## 🔐 密碼驗證範例

```python
from pydantic import BaseModel, Field, field_validator
import re

class PasswordPolicy(BaseModel):
    &#34;&#34;&#34;密碼政策驗證&#34;&#34;&#34;
    password: str = Field(
        ...,
        min_length=8,
        max_length=128,
        description=&#34;密碼&#34;
    )

    @field_validator(&#39;password&#39;)
    @classmethod
    def validate_password_strength(cls, v: str) -&gt; str:
        errors = []

        if not re.search(r&#39;[A-Z]&#39;, v):
            errors.append(&#34;需要至少一個大寫字母&#34;)

        if not re.search(r&#39;[a-z]&#39;, v):
            errors.append(&#34;需要至少一個小寫字母&#34;)

        if not re.search(r&#39;\d&#39;, v):
            errors.append(&#34;需要至少一個數字&#34;)

        if not re.search(r&#39;[!@#$%^&amp;*(),.?&#34;:{}|&lt;&gt;]&#39;, v):
            errors.append(&#34;需要至少一個特殊字元&#34;)

        if errors:
            raise ValueError(&#34;; &#34;.join(errors))

        return v

class UserRegistration(BaseModel):
    username: str = Field(..., min_length=3, max_length=30)
    email: str = Field(..., pattern=r&#39;^[\w\.-]&#43;@[\w\.-]&#43;\.\w&#43;$&#39;)
    password: str = Field(..., min_length=8)
    confirm_password: str

    @field_validator(&#39;password&#39;)
    @classmethod
    def validate_password(cls, v: str) -&gt; str:
        if not re.search(r&#39;[A-Z]&#39;, v):
            raise ValueError(&#34;密碼需要至少一個大寫字母&#34;)
        if not re.search(r&#39;[a-z]&#39;, v):
            raise ValueError(&#34;密碼需要至少一個小寫字母&#34;)
        if not re.search(r&#39;\d&#39;, v):
            raise ValueError(&#34;密碼需要至少一個數字&#34;)
        return v

    @field_validator(&#39;confirm_password&#39;)
    @classmethod
    def passwords_match(cls, v: str, info) -&gt; str:
        if &#39;password&#39; in info.data and v != info.data[&#39;password&#39;]:
            raise ValueError(&#34;密碼不一致&#34;)
        return v
```

---

## 📝 實戰範例：API 請求驗證

```python
from pydantic import BaseModel, Field, EmailStr, field_validator
from typing import Optional, List
from datetime import date
from enum import Enum

class Gender(str, Enum):
    MALE = &#34;male&#34;
    FEMALE = &#34;female&#34;
    OTHER = &#34;other&#34;

class Address(BaseModel):
    &#34;&#34;&#34;地址&#34;&#34;&#34;
    city: str = Field(..., min_length=1, max_length=50)
    district: str = Field(..., min_length=1, max_length=50)
    street: str = Field(..., min_length=1, max_length=200)
    zip_code: str = Field(..., pattern=r&#34;^\d{3,5}$&#34;)

class UserRegistrationRequest(BaseModel):
    &#34;&#34;&#34;使用者註冊請求&#34;&#34;&#34;

    # 基本資訊
    username: str = Field(
        ...,
        min_length=3,
        max_length=30,
        pattern=r&#34;^[a-zA-Z][a-zA-Z0-9_]*$&#34;,
        description=&#34;使用者名稱，字母開頭，只能包含字母、數字、底線&#34;
    )

    email: EmailStr = Field(
        ...,
        description=&#34;電子郵件&#34;
    )

    password: str = Field(
        ...,
        min_length=8,
        max_length=128,
        description=&#34;密碼，至少 8 個字元&#34;
    )

    # 個人資訊
    full_name: str = Field(
        ...,
        min_length=1,
        max_length=100,
        description=&#34;全名&#34;
    )

    phone: Optional[str] = Field(
        None,
        pattern=r&#34;^09\d{8}$&#34;,
        description=&#34;手機號碼&#34;
    )

    birth_date: Optional[date] = Field(
        None,
        description=&#34;生日&#34;
    )

    gender: Optional[Gender] = None

    # 地址
    address: Optional[Address] = None

    # 偏好設定
    receive_newsletter: bool = Field(
        default=False,
        description=&#34;是否接收電子報&#34;
    )

    interests: List[str] = Field(
        default_factory=list,
        max_length=10,
        description=&#34;興趣標籤，最多 10 個&#34;
    )

    @field_validator(&#39;birth_date&#39;)
    @classmethod
    def validate_birth_date(cls, v: Optional[date]) -&gt; Optional[date]:
        if v is not None:
            today = date.today()
            age = today.year - v.year - ((today.month, today.day) &lt; (v.month, v.day))
            if age &lt; 13:
                raise ValueError(&#34;必須年滿 13 歲&#34;)
            if age &gt; 120:
                raise ValueError(&#34;年齡不合理&#34;)
        return v

    @field_validator(&#39;interests&#39;)
    @classmethod
    def validate_interests(cls, v: List[str]) -&gt; List[str]:
        # 去重並轉小寫
        return list(set(item.lower().strip() for item in v if item.strip()))
```

---

## ✅ 重點總結

### 驗證約束速查表

| 約束 | 適用型別 | 範例 |
|------|----------|------|
| `gt`, `ge`, `lt`, `le` | 數字 | `Field(ge=0, le=100)` |
| `multiple_of` | 數字 | `Field(multiple_of=5)` |
| `min_length`, `max_length` | 字串/列表 | `Field(min_length=1)` |
| `pattern` | 字串 | `Field(pattern=r&#34;^\d&#43;$&#34;)` |
| `Literal` | 任意 | `Literal[&#34;a&#34;, &#34;b&#34;]` |
| `Enum` | 任意 | `class Status(str, Enum)` |

### 常用特殊型別

| 型別 | 用途 |
|------|------|
| `EmailStr` | Email 驗證 |
| `HttpUrl` | URL 驗證 |
| `SecretStr` | 敏感資料 |
| `IPvAnyAddress` | IP 位址 |
| `UUID4` | UUID v4 |

---

## 🎤 面試這樣答

### Q: Pydantic 如何驗證一個欄位必須是正數？

**答案：**

&gt; 使用 Field 的 gt 或 ge 參數：
&gt;
&gt; ```python
&gt; from pydantic import BaseModel, Field
&gt;
&gt; class Product(BaseModel):
&gt;     price: float = Field(gt=0)      # 大於 0
&gt;     quantity: int = Field(ge=1)     # 大於等於 1
&gt; ```
&gt;
&gt; gt = greater than（大於）
&gt; ge = greater than or equal（大於等於）

---

## 🤓 小測驗

1. 如何限制字串只能是特定的幾個值？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   使用 Literal 或 Enum，例如：`status: Literal[&#34;active&#34;, &#34;inactive&#34;]`
   &lt;/details&gt;

2. `min_length` 可以用在什麼型別？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   字串和列表（包括 List, Set 等容器型別）
   &lt;/details&gt;

3. 如何驗證 email 格式？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   使用 EmailStr 型別：`email: EmailStr`
   &lt;/details&gt;

---

**上一篇：** [02-1. Pydantic 基礎模型](./02-1)
**下一篇：** [02-3. 自訂驗證器](./02-3)

---

最後更新：2025-12-17


---

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

