4. Pydantic数据验证:模型定义、字段类型、嵌套模型、验证器与自定义校验逻辑
在FastAPI的世界里,Pydantic不仅仅是一个数据验证库,它是整个API框架的"数据守门员"。当请求进入你的应用时,Pydantic负责将原始的JSON数据转换为Python对象,同时确保数据的类型、格式和业务约束都得到满足。这一章节将深入探讨Pydantic的核心机制——从基础模型定义到复杂的自定义验证器,帮助你构建既健壮又灵活的API数据层。
4.1 模型定义与字段类型:从基础到进阶
核心原理:Pydantic模型本质上是一个继承自BaseModel的Python类。每个类属性都对应一个字段,其类型注解决定了数据验证的规则。Pydantic v2使用Python的typing模块进行类型解析,支持从简单的str、int到复杂的泛型类型如List[Dict[str, int]]。
from pydantic import BaseModel, EmailStr, Field
from typing import Optional, List
from datetime import datetime
class UserProfile(BaseModel):
username: str = Field(..., min_length=3, max_length=20, description="用户名")
email: EmailStr
age: int = Field(20, ge=0, le=150)
tags: List[str] = []
created_at: datetime = Field(default_factory=datetime.now)
bio: Optional[str] = None
操作流程:定义模型时,需要遵循以下步骤:
- 继承
BaseModel类 - 为每个字段声明类型注解
- 使用
Field()函数添加约束条件(如长度、范围、正则) - 设置默认值或
default_factory处理动态默认值 - 使用
Optional[X]或X | None标记可选字段
| 字段类型 | 适用场景 | 常用约束 | 示例 |
|---|---|---|---|
| str | 文本输入 | min_length, max_length, pattern | Field(min_length=2) |
| int | 整数 | ge, le, gt, lt, multiple_of | Field(ge=0, le=100) |
| float | 浮点数 | ge, le, allow_inf_nan | Field(gt=0) |
| bool | 布尔值 | — | is_active: bool = True |
| EmailStr | 邮箱验证 | 自动格式校验 | email: EmailStr |
| HttpUrl | URL验证 | scheme, host检查 | url: HttpUrl |
| UUID | 唯一标识 | 格式校验 | id: UUID |
| datetime | 时间戳 | 时区处理 | created: datetime |
| constr | 受限字符串 | regex, strip_whitespace | constr(pattern=r'^\d{5}$') |
| conint | 受限整数 | ge, le, multiple_of | conint(ge=1, le=10) |
def get_user(user_id: int = Path(..., ge=1))会自动验证路径参数必须为正整数。
4.2 嵌套模型:构建复杂数据结构
核心原理:嵌套模型允许在一个Pydantic模型中引用其他模型作为字段类型。这种设计模式完美映射了JSON的层级结构,使得复杂的数据关系能够被清晰表达和验证。当处理嵌套模型时,Pydantic会递归地验证所有子模型。
from pydantic import BaseModel
from typing import List, Optional
class Address(BaseModel):
street: str
city: str
zip_code: str = Field(pattern=r'^\d{5}$')
class User(BaseModel):
name: str
address: Address # 嵌套模型
emergency_contacts: List[Address] = [] # 嵌套模型列表
# 使用示例
user_data = {
"name": "张三",
"address": {"street": "科技路1号", "city": "北京", "zip_code": "100000"},
"emergency_contacts": [
{"street": "学院路2号", "city": "上海", "zip_code": "200000"}
]
}
user = User(**user_data)
print(user.address.city) # 输出: 北京
实战场景:在电商系统中,订单模型通常包含用户信息、商品列表、支付信息等多个嵌套结构。使用嵌套模型可以确保每个层级的数据都经过验证,避免脏数据进入业务逻辑层。
model_validator进行跨字段验证。同时,注意循环引用问题——模型A引用模型B,模型B又引用模型A会导致递归错误。
4.3 验证器:字段级与模型级自定义校验
核心原理:Pydantic v2引入了@field_validator和@model_validator装饰器,分别用于字段级和模型级的自定义验证逻辑。字段验证器在单个字段验证后执行,而模型验证器在整个模型验证完成后执行,可以访问所有字段的值。
from pydantic import BaseModel, field_validator, model_validator
from typing import Any
class Order(BaseModel):
quantity: int
price: float
discount: float = 0.0
@field_validator('quantity')
@classmethod
def validate_quantity(cls, v: int) -> int:
if v < 1:
raise ValueError('数量必须大于0')
if v > 1000:
raise ValueError('单次下单数量不能超过1000')
return v
@field_validator('discount')
@classmethod
def validate_discount(cls, v: float) -> float:
if not 0 <= v <= 0.5:
raise ValueError('折扣必须在0到50%之间')
return v
@model_validator(mode='after')
def validate_total(self) -> 'Order':
total = self.quantity * self.price * (1 - self.discount)
if total > 100000:
raise ValueError('订单总金额超过上限')
return self
操作流程:编写验证器时需注意:
- 使用
@classmethod装饰器,第一个参数为cls - 字段验证器接收
v(待验证值),返回验证后的值 - 验证失败时抛出
ValueError或AssertionError - 模型验证器使用
mode='after'在字段验证后执行 - 验证器可以访问
self获取其他字段值(模型级)
场景:用户注册API需要确认密码字段与密码字段一致。
实现:使用
@model_validator比较两个字段值。关键点:模型级验证器可以访问所有字段,适合跨字段逻辑校验。
4.4 自定义校验逻辑与高级技巧
核心原理:除了内置验证器,Pydantic还支持通过Annotated类型和自定义类型实现更灵活的验证。在FastAPI中,这些自定义验证逻辑可以直接用于请求体、查询参数和路径参数。
from typing import Annotated
from pydantic import BaseModel, BeforeValidator, AfterValidator
def validate_phone(v: str) -> str:
"""自定义手机号验证"""
if not v.startswith('+86'):
raise ValueError('仅支持中国大陆手机号')
if len(v) != 13:
raise ValueError('手机号长度不正确')
return v
PhoneNumber = Annotated[str, BeforeValidator(validate_phone)]
class Contact(BaseModel):
phone: PhoneNumber
name: str
# FastAPI中使用
@app.post("/contact/")
async def create_contact(contact: Contact):
return {"message": f"联系人 {contact.name} 创建成功"}
常见误区:
- 误区1:在验证器中修改其他字段的值——验证器应保持单一职责,修改其他字段应使用
model_validator - 误区2:忽略
@classmethod装饰器——Pydantic v2要求验证器必须是类方法 - 误区3:过度使用正则表达式——对于简单格式校验,优先使用内置类型如
EmailStr - 误区4:在验证器中执行数据库查询——验证器应保持纯函数特性,数据库操作应放在业务逻辑层
model_config = ConfigDict(validate_assignment=True)在属性赋值时也进行验证,但要注意这会增加运行时开销。对于只读数据,建议关闭此选项。
4.5 实战案例:用户注册API的数据验证闭环
案例背景:某SaaS平台需要实现用户注册功能,要求支持邮箱/手机号注册,密码强度校验,以及用户资料的可选填写。初始版本因验证不严格导致大量垃圾账号注册。
关键输入数据:
- 注册请求:
{"email": "user@example.com", "password": "abc123", "profile": {"nickname": "测试用户", "age": 25}} - 业务规则:密码至少8位且包含字母和数字;邮箱必须验证格式;年龄范围18-100
实现方案:
from pydantic import BaseModel, EmailStr, field_validator, model_validator
from typing import Optional
class UserProfile(BaseModel):
nickname: str = Field(min_length=2, max_length=30)
age: Optional[int] = Field(None, ge=18, le=100)
bio: Optional[str] = Field(None, max_length=500)
class RegisterRequest(BaseModel):
email: EmailStr
password: str
profile: UserProfile
@field_validator('password')
@classmethod
def validate_password(cls, v: str) -> str:
if len(v) < 8:
raise ValueError('密码至少8位')
if not any(c.isalpha() for c in v) or not any(c.isdigit() for c in v):
raise ValueError('密码必须包含字母和数字')
return v
@model_validator(mode='after')
def validate_email_domain(self) -> 'RegisterRequest':
# 禁止使用临时邮箱域名
blocked_domains = ['mailinator.com', 'yopmail.com']
domain = self.email.split('@')[1]
if domain in blocked_domains:
raise ValueError('该邮箱域名已被禁止注册')
return self
结果对比:实施严格验证后,垃圾账号注册量下降87%,无效数据导致的业务错误减少92%。API响应时间仅增加约3ms(验证开销),远低于业务逻辑处理时间。
复盘经验:
- 验证规则应分层:基础格式验证(类型、长度)→ 业务规则验证(密码强度、域名黑名单)
- 错误信息要具体,便于前端展示友好提示
- 对于复杂验证逻辑,使用
Annotated类型可以复用验证规则 - 在FastAPI中,验证错误会自动转换为422响应,包含详细的错误路径和原因
通过本章的学习,你已经掌握了Pydantic数据验证的核心能力。从简单的字段类型到复杂的嵌套模型,从内置验证器到自定义校验逻辑,这些工具将帮助你在FastAPI中构建出既安全又高效的数据层。记住,好的数据验证是API质量的第一道防线,它不仅能防止错误数据进入系统,还能提升开发效率和用户体验。