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对象属性读取数据,而不是要求字典。

Pydantic 校验与序列化数据流 HTTP 请求体 JSON / 表单 类型转换 str→int / 宽松模式 字段约束 长度 / 范围 / 正则 自定义验证器 跨字段 / 业务规则 嵌套模型递归校验 OrderCreate.items[0].quantity → 错误路径 items.0.quantity 任意子项失败 → 整体 ValidationError 校验通过 得到模型实例 → 业务处理 校验失败 422 + detail 字段路径 响应序列化 response_model 过滤字段

自定义验证器: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的输入输出就会既安全又高效。