4. Pydantic数据校验与序列化
在FastAPI中,请求体解析、查询参数校验、响应序列化几乎全部依赖Pydantic。理解Pydantic的工作方式,直接决定了API的健壮性与吞吐能力。本节围绕模型定义、字段验证、嵌套模型、自定义验证器与性能考量五个层面展开,重点说明“怎么写”以及“为什么这样写会影响性能”。
模型定义:从类型注解到校验规则
Pydantic模型通过继承 BaseModel 定义,字段使用Python类型注解声明。FastAPI在启动时会读取这些注解,生成校验与序列化逻辑。一个典型的用户创建模型如下:
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=32, pattern=r"^[a-zA-Z0-9_]+$")
email: EmailStr
age: int = Field(ge=18, le=120)
tags: list[str] = Field(default_factory=list, max_length=10)
created_at: datetime | None = None
这里 Field 承担了约束声明:min_length、max_length 限制字符串长度,pattern 使用正则约束字符集,ge/le 限定数值范围,default_factory 避免可变默认值共享。需要注意,EmailStr 依赖 email-validator 包,未安装时导入会失败。字段默认值应使用不可变对象;列表、字典必须用 default_factory,否则所有实例会共享同一个可变对象,造成难以排查的串数据问题。
字段验证:内置约束与校验时机
Pydantic的校验发生在模型实例化时。FastAPI收到请求后,先解析JSON,再交给模型校验;校验失败会抛出 ValidationError,FastAPI将其转换为422响应,并在 detail 中给出字段路径、错误类型与输入值。校验顺序大致为:类型转换 → 字段约束 → 模型级验证器。类型转换是宽松的,例如字符串 "25" 可以转为整数 25,但 "abc" 会失败。若希望严格模式,可使用 model_config = ConfigDict(strict=True),此时字符串不再自动转整数。
字段约束应尽量前置到声明中,而不是全部塞进自定义验证器。声明式约束由Pydantic核心用编译后的校验器执行,速度远快于Python函数调用。只有跨字段依赖、外部查询或复杂业务规则,才值得写自定义验证器。
嵌套模型:结构复用与序列化边界
嵌套模型用于表达层级数据,例如订单包含多个商品项:
class OrderItem(BaseModel):
sku: str
quantity: int = Field(gt=0)
price: float = Field(gt=0)
class OrderCreate(BaseModel):
order_no: str
items: list[OrderItem] = Field(min_length=1)
remark: str | None = None
嵌套校验是递归的:items 中任意一项的 quantity 不合法,错误路径会显示为 items.0.quantity,便于前端定位。响应模型常用 response_model 指定,FastAPI会按该模型过滤字段,避免把数据库中的密码哈希等敏感字段返回。若响应模型与内部模型字段不一致,可在模型上使用 model_config = ConfigDict(from_attributes=True),让Pydantic从ORM对象属性读取数据,而不是要求字典。
自定义验证器:field_validator 与 model_validator
Pydantic v2 提供 @field_validator 与 @model_validator。前者作用于单个字段,后者作用于整个模型。例如要求用户名不能是保留字,且订单总价与明细一致:
from pydantic import field_validator, model_validator
class UserCreate(BaseModel):
username: str
email: EmailStr
@field_validator("username")
@classmethod
def not_reserved(cls, v: str) -> str:
if v.lower() in {"admin", "root", "system"}:
raise ValueError("用户名不可为系统保留字")
return v
class OrderCreate(BaseModel):
items: list[OrderItem]
total: float
@model_validator(mode="after")
def check_total(self):
calc = sum(i.price * i.quantity for i in self.items)
if abs(calc - self.total) > 0.01:
raise ValueError(f"总价不一致,应为 {calc:.2f}")
return self
mode="after" 表示在字段校验完成后执行,此时 self.items 已是合法模型列表。若需要访问原始输入,可用 mode="before"。自定义验证器应保持纯函数、无副作用,避免在其中做数据库查询或网络调用,否则会显著拖慢请求处理,并让错误处理变得复杂。
性能考量:把开销放在正确的位置
Pydantic v2 的核心校验器由 Rust 实现,比 v1 的纯 Python 校验快数倍。性能优化应遵循以下原则:第一,优先使用声明式约束,减少自定义验证器数量;第二,避免在模型层做I/O,把外部依赖校验移到服务层;第三,响应模型只声明需要返回的字段,减少序列化开销;第四,对高频小对象,可考虑使用 model_construct 跳过校验,但仅限内部可信数据。此外,model_config = ConfigDict(extra="forbid") 可拒绝多余字段,防止意外数据进入;validate_assignment=True 会在赋值时重新校验,适合可变模型,但会带来额外开销,需按场景取舍。
Field 表达所有能表达的约束,再写自定义验证器;自定义验证器只处理跨字段与业务规则。这样既保证可读性,也让Pydantic的编译校验路径发挥最大性能。综合来看,Pydantic在FastAPI中既是数据入口的守门人,也是响应出口的过滤器。模型定义决定校验强度,嵌套结构决定错误定位粒度,自定义验证器决定业务规则边界,而性能考量决定这些能力是否以合理成本运行。把这四者配合好,API的输入输出就会既安全又高效。