目錄
02-2. 欄位驗證與約束
⏱️ 閱讀時間: 15 分鐘 🎯 難度: ⭐⭐ (基礎)
🤔 一句話解釋
Pydantic 提供豐富的內建驗證器,讓你用簡單的參數就能定義複雜的驗證規則。
🔢 數字驗證
基本約束
from pydantic import BaseModel, Field
class NumberConstraints(BaseModel):
# 大於 (greater than)
gt_example: int = Field(gt=0) # > 0
# 大於等於 (greater than or equal)
ge_example: int = Field(ge=0) # >= 0
# 小於 (less than)
lt_example: int = Field(lt=100) # < 100
# 小於等於 (less than or equal)
le_example: int = Field(le=100) # <= 100
# 組合使用
age: int = Field(ge=0, le=150) # 0 <= age <= 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
)實際應用
from pydantic import BaseModel, Field
from decimal import Decimal
class PaymentRequest(BaseModel):
"""付款請求"""
amount: Decimal = Field(
...,
gt=0,
le=Decimal("1000000.00"),
decimal_places=2,
description="付款金額,最多 100 萬"
)
installments: int = Field(
default=1,
ge=1,
le=24,
description="分期期數,1-24 期"
)
class DiscountCoupon(BaseModel):
"""折扣券"""
discount_percent: int = Field(
...,
ge=1,
le=100,
description="折扣百分比,1-100"
)
min_order_amount: Decimal = Field(
default=Decimal("0"),
ge=0,
description="最低訂單金額"
)
max_discount: Decimal = Field(
default=Decimal("1000"),
gt=0,
description="最高折抵金額"
)📝 字串驗證
長度約束
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"^[A-Z]{2}$") # 兩個大寫字母正規表達式驗證
from pydantic import BaseModel, Field
class PatternValidation(BaseModel):
# 電話號碼(台灣手機)
phone: str = Field(
pattern=r"^09\d{8}$",
description="台灣手機號碼"
)
# 身分證字號
taiwan_id: str = Field(
pattern=r"^[A-Z][12]\d{8}$",
description="台灣身分證字號"
)
# 郵遞區號
zip_code: str = Field(
pattern=r"^\d{3,5}$",
description="郵遞區號"
)
# Slug(URL 友善字串)
slug: str = Field(
pattern=r"^[a-z0-9]+(?:-[a-z0-9]+)*$",
description="URL slug"
)
# 密碼(至少一個大寫、一個小寫、一個數字)
strong_password: str = Field(
pattern=r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$",
description="強密碼"
)字串處理
from pydantic import BaseModel, Field
from typing import Annotated
from pydantic.functional_validators import AfterValidator
def normalize_email(v: str) -> str:
return v.lower().strip()
def strip_whitespace(v: str) -> str:
return v.strip()
class UserInput(BaseModel):
# 使用 Annotated 添加處理
email: Annotated[str, AfterValidator(normalize_email)]
name: Annotated[str, AfterValidator(strip_whitespace)]
# 測試
user = UserInput(email=" JOHN@EXAMPLE.COM ", name=" John Doe ")
print(user.email) # john@example.com
print(user.name) # John Doe📋 列表與集合驗證
長度約束
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="標籤,最多 10 個"
)
# 集合
categories: Set[str] = Field(
default_factory=set,
max_length=5,
description="分類,最多 5 個"
)
# 非空列表
items: List[int] = Field(
...,
min_length=1,
description="至少要有一個項目"
)列表元素驗證
from pydantic import BaseModel, Field
from typing import List
from typing_extensions import Annotated
class Order(BaseModel):
# 列表中的每個元素都會被驗證
quantities: List[int] = Field(
...,
description="每個項目的數量"
)
# 使用 Annotated 對元素進行約束
prices: List[Annotated[float, Field(gt=0)]] = Field(
default_factory=list,
description="價格列表,每個價格必須大於 0"
)📧 內建特殊型別
Email 驗證
from pydantic import BaseModel, EmailStr
class User(BaseModel):
email: EmailStr # 自動驗證 email 格式
# 測試
User(email="john@example.com") # ✅
User(email="invalid-email") # ❌ ValidationErrorURL 驗證
from pydantic import BaseModel, HttpUrl, AnyUrl
class Links(BaseModel):
# HTTP/HTTPS URL
website: HttpUrl
# 任意 URL(包括 ftp, file 等)
resource: AnyUrl
# 測試
Links(
website="https://example.com",
resource="ftp://files.example.com/data"
)其他特殊型別
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="192.168.1.1",
password="secret123",
user_id="550e8400-e29b-41d4-a716-446655440000",
metadata='{"key": "value"}'
)
print(user.password) # SecretStr('**********')
print(user.password.get_secret_value()) # secret123🎛️ 條件約束
使用 Literal 限制值
from pydantic import BaseModel
from typing import Literal
class Config(BaseModel):
environment: Literal["development", "staging", "production"]
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]
# 測試
Config(environment="production", log_level="INFO") # ✅
Config(environment="invalid", log_level="INFO") # ❌使用 Enum
from pydantic import BaseModel
from enum import Enum
class Status(str, Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
class OrderStatus(str, Enum):
CREATED = "created"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Order(BaseModel):
id: int
status: OrderStatus = OrderStatus.CREATED
# 使用
order = Order(id=1, status="paid") # 字串會轉成 Enum
print(order.status) # OrderStatus.PAID
print(order.status.value) # paid聯合型別
from pydantic import BaseModel
from typing import Union, List
class StringOrList(BaseModel):
# 可以是字串或字串列表
tags: Union[str, List[str]]
# 兩種都可以
StringOrList(tags="python")
StringOrList(tags=["python", "fastapi"])
# Python 3.10+ 語法
class StringOrList2(BaseModel):
tags: str | list[str]🔐 密碼驗證範例
from pydantic import BaseModel, Field, field_validator
import re
class PasswordPolicy(BaseModel):
"""密碼政策驗證"""
password: str = Field(
...,
min_length=8,
max_length=128,
description="密碼"
)
@field_validator('password')
@classmethod
def validate_password_strength(cls, v: str) -> str:
errors = []
if not re.search(r'[A-Z]', v):
errors.append("需要至少一個大寫字母")
if not re.search(r'[a-z]', v):
errors.append("需要至少一個小寫字母")
if not re.search(r'\d', v):
errors.append("需要至少一個數字")
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', v):
errors.append("需要至少一個特殊字元")
if errors:
raise ValueError("; ".join(errors))
return v
class UserRegistration(BaseModel):
username: str = Field(..., min_length=3, max_length=30)
email: str = Field(..., pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
password: str = Field(..., min_length=8)
confirm_password: str
@field_validator('password')
@classmethod
def validate_password(cls, v: str) -> str:
if not re.search(r'[A-Z]', v):
raise ValueError("密碼需要至少一個大寫字母")
if not re.search(r'[a-z]', v):
raise ValueError("密碼需要至少一個小寫字母")
if not re.search(r'\d', v):
raise ValueError("密碼需要至少一個數字")
return v
@field_validator('confirm_password')
@classmethod
def passwords_match(cls, v: str, info) -> str:
if 'password' in info.data and v != info.data['password']:
raise ValueError("密碼不一致")
return v📝 實戰範例:API 請求驗證
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 = "male"
FEMALE = "female"
OTHER = "other"
class Address(BaseModel):
"""地址"""
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"^\d{3,5}$")
class UserRegistrationRequest(BaseModel):
"""使用者註冊請求"""
# 基本資訊
username: str = Field(
...,
min_length=3,
max_length=30,
pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$",
description="使用者名稱,字母開頭,只能包含字母、數字、底線"
)
email: EmailStr = Field(
...,
description="電子郵件"
)
password: str = Field(
...,
min_length=8,
max_length=128,
description="密碼,至少 8 個字元"
)
# 個人資訊
full_name: str = Field(
...,
min_length=1,
max_length=100,
description="全名"
)
phone: Optional[str] = Field(
None,
pattern=r"^09\d{8}$",
description="手機號碼"
)
birth_date: Optional[date] = Field(
None,
description="生日"
)
gender: Optional[Gender] = None
# 地址
address: Optional[Address] = None
# 偏好設定
receive_newsletter: bool = Field(
default=False,
description="是否接收電子報"
)
interests: List[str] = Field(
default_factory=list,
max_length=10,
description="興趣標籤,最多 10 個"
)
@field_validator('birth_date')
@classmethod
def validate_birth_date(cls, v: Optional[date]) -> Optional[date]:
if v is not None:
today = date.today()
age = today.year - v.year - ((today.month, today.day) < (v.month, v.day))
if age < 13:
raise ValueError("必須年滿 13 歲")
if age > 120:
raise ValueError("年齡不合理")
return v
@field_validator('interests')
@classmethod
def validate_interests(cls, v: List[str]) -> 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"^\d+$") |
Literal | 任意 | Literal["a", "b"] |
Enum | 任意 | class Status(str, Enum) |
常用特殊型別
| 型別 | 用途 |
|---|---|
EmailStr | Email 驗證 |
HttpUrl | URL 驗證 |
SecretStr | 敏感資料 |
IPvAnyAddress | IP 位址 |
UUID4 | UUID v4 |
🎤 面試這樣答
Q: Pydantic 如何驗證一個欄位必須是正數?
答案:
使用 Field 的 gt 或 ge 參數:
from pydantic import BaseModel, Field class Product(BaseModel): price: float = Field(gt=0) # 大於 0 quantity: int = Field(ge=1) # 大於等於 1gt = greater than(大於) ge = greater than or equal(大於等於)
🤓 小測驗
如何限制字串只能是特定的幾個值?
min_length可以用在什麼型別?如何驗證 email 格式?
上一篇: 02-1. Pydantic 基礎模型 下一篇: 02-3. 自訂驗證器
最後更新:2025-12-17