4、Pydantic模型与数据验证:字段类型、必填与默认值、嵌套模型、自定义验证器、模型配置与序列化
在FastAPI的世界里,Pydantic模型不仅是请求与响应数据的"形状定义器",更是保障API健壮性的第一道防线。它通过Python类型注解(Type Hints)在运行时执行严格的数据校验、类型转换与序列化,将不可信的HTTP请求体转化为类型安全、结构清晰的Python对象。本章将深入剖析Pydantic v2的核心机制,从字段声明到高级验证器,再到模型配置与序列化策略,帮助你构建既严谨又灵活的API数据层。
上图展示了Pydantic模型在FastAPI请求处理链路中的核心位置。从HTTP请求体到模型实例,再到序列化输出,每一步都贯穿着类型检查与数据验证。接下来,我们将逐一拆解这些环节的具体实现与最佳实践。
4.1 字段类型体系:从基础到高级
核心原理:Pydantic v2的字段类型系统建立在Python标准类型(如str、int、float、bool)之上,并扩展了大量语义化类型。这些类型不仅定义了数据的"形状",还隐含了验证规则。例如,EmailStr会自动校验字符串是否为合法邮箱格式,UUID会验证并转换UUID字符串。
判断方法:选择字段类型时,应遵循"最小充分原则"——在满足业务需求的前提下,选择约束最严格的类型。例如,对于年龄字段,使用int配合Field(ge=0, le=150)比单纯使用int更安全;对于订单号,使用UUID比使用str更能保证唯一性与格式正确性。
| 类型 | 验证规则 | 典型场景 |
|---|---|---|
constr(min_length=2, max_length=50) | 字符串长度限制 | 用户名、产品名称 |
conint(ge=0, le=100) | 整数范围限制 | 折扣百分比、年龄 |
EmailStr | 邮箱格式校验 | 用户注册、联系邮箱 |
IPvAnyAddress | IPv4/IPv6地址校验 | 服务器IP配置 |
Literal["pending", "paid"] | 枚举值限制 | 订单状态机 |
实战案例:在电商API中定义商品模型时,我们不仅需要基础类型,还需要对价格、库存等字段施加业务约束。
from pydantic import BaseModel, Field, EmailStr
from typing import Literal
from uuid import UUID
class Product(BaseModel):
id: UUID
name: str = Field(..., min_length=3, max_length=100)
price: float = Field(..., gt=0, le=100000)
stock: int = Field(..., ge=0)
status: Literal["active", "inactive"] = "active"
supplier_email: EmailStr | None = None
这里Field(..., min_length=3)中的...表示该字段为必填项。通过类型与Field的组合,我们在一行代码内完成了格式、范围、必填性三重验证。
常见误区:不要过度依赖float处理金额。由于浮点数精度问题,建议使用Decimal类型或整数(以分为单位)来存储金额。例如:price_cents: int = Field(..., ge=0)。
4.2 必填与默认值:灵活性与安全性的平衡
核心原理:Pydantic通过字段声明方式区分必填与可选。使用字段名: 类型声明必填字段;使用字段名: 类型 = 默认值声明可选字段。默认值可以是静态值,也可以是Field(default_factory=...)动态生成的值(如当前时间、UUID)。
操作流程:在定义模型时,应遵循以下决策路径:
- 该字段在业务上是否必须存在?若是,则声明为必填。
- 若可选,是否有合理的默认值?若有,直接赋值;若默认值需要动态生成(如创建时间),使用
default_factory。 - 若字段可能为
None,使用类型 | None联合类型,并设置默认值为None。
from datetime import datetime
from uuid import uuid4
class Order(BaseModel):
order_id: str = Field(default_factory=lambda: str(uuid4()))
created_at: datetime = Field(default_factory=datetime.utcnow)
paid_at: datetime | None = None
coupon_code: str | None = None
经验提醒:使用default_factory而非default=datetime.utcnow()。后者在模型定义时(模块导入时)只执行一次,导致所有实例共享同一个时间戳;前者在每次实例化时执行,确保每个订单的创建时间独立。
常见误区:将可变对象(如列表、字典)作为默认值。Pydantic v2会强制要求使用Field(default_factory=list),直接使用[]会抛出ValueError,这避免了多个实例共享同一可变对象的经典Python陷阱。
4.3 嵌套模型:构建复杂数据结构
核心原理:嵌套模型允许将一个Pydantic模型作为另一个模型的字段类型。这天然支持了JSON的层级结构,使得API可以接收和返回复杂的嵌套数据。Pydantic会自动递归验证嵌套模型的所有字段。
实战场景:在用户管理系统中,一个用户拥有多个地址,每个地址包含省、市、街道等信息。
class Address(BaseModel):
province: str
city: str
street: str
postal_code: str = Field(..., pattern=r"^\d{6}$")
class User(BaseModel):
name: str
email: EmailStr
addresses: list[Address] # 嵌套模型列表
primary_address: Address | None = None # 可选嵌套模型
当请求体为{"name": "张三", "email": "zhangsan@example.com", "addresses": [{"province": "广东", "city": "深圳", "street": "科技园路", "postal_code": "518000"}]}时,Pydantic会自动将addresses列表中的每个字典转换为Address实例。若某个地址缺少province字段,验证将失败并返回详细的错误路径(如addresses.0.province)。
案例拆解:嵌套模型在FastAPI响应模型中的应用。假设我们需要返回用户及其订单列表,但不想暴露内部字段:
class OrderOut(BaseModel):
order_id: str
total_amount: float
status: str
class UserOut(BaseModel):
id: int
name: str
orders: list[OrderOut] = []
model_config = {"from_attributes": True} # 允许从ORM对象转换
在路由中声明response_model=UserOut,FastAPI会自动过滤掉未在模型中定义的字段,并递归序列化嵌套模型。这实现了响应数据的"最小化暴露"。
性能注意:深度嵌套(超过5层)会显著增加验证时间。对于高并发接口,建议将嵌套层级控制在3层以内,或使用model_dump(exclude_none=True)减少序列化数据量。
4.4 自定义验证器:超越类型注解的约束
核心原理:当内置类型和Field约束无法表达复杂业务规则时,使用@field_validator(字段级)和@model_validator(模型级)装饰器编写自定义验证逻辑。Pydantic v2的验证器支持mode="before"(在类型转换前执行)和mode="after"(在类型转换后执行)。
操作流程:
- 确定验证时机:需要原始输入(如字符串)时用
mode="before";需要转换后的值(如int)时用mode="after"。 - 编写验证函数:函数名建议以
validate_开头,接收字段值(或整个模型)作为参数。 - 抛出
ValueError或AssertionError触发验证失败。
from pydantic import BaseModel, field_validator, model_validator
class Booking(BaseModel):
start_date: datetime
end_date: datetime
room_count: int
@field_validator("end_date")
@classmethod
def check_end_after_start(cls, v, info):
if "start_date" in info.data and v <= info.data["start_date"]:
raise ValueError("结束日期必须晚于开始日期")
return v
@model_validator(mode="after")
def check_room_availability(self):
if self.room_count > 5:
raise ValueError("单次预订不能超过5间房")
return self
案例拆解:在预订系统中,我们使用@field_validator确保结束日期晚于开始日期。注意info.data包含已验证的字段值,但字段顺序会影响可用性——start_date必须声明在end_date之前。若需要跨字段复杂校验,使用@model_validator(mode="after")更稳妥,此时所有字段均已验证完毕。
经验提醒:验证器应保持"单一职责"——一个验证器只检查一条规则。这样错误信息更清晰,且便于单元测试。若验证逻辑涉及外部服务(如数据库查询),请将其放在路由处理函数中,而非验证器内,以避免模型层与基础设施耦合。
4.5 模型配置与序列化:控制输出与行为
核心原理:通过model_config = ConfigDict(...)可以控制模型的全局行为。常用配置包括:extra(处理多余字段)、str_strip(去除字符串首尾空格)、populate_by_name(允许使用字段别名)、from_attributes(从ORM对象初始化)。序列化则通过model_dump()(返回字典)和model_dump_json()(返回JSON字符串)实现。
| 配置项 | 取值 | 作用 |
|---|---|---|
extra | "ignore" / "forbid" / "allow" | 忽略、禁止或保留多余字段 |
str_strip | True / False | 自动去除字符串字段首尾空白 |
populate_by_name | True / False | 允许使用字段名(而非别名)填充数据 |
ser_json_timedelta | "iso8601" / "float" | 时间间隔序列化格式 |
from pydantic import BaseModel, ConfigDict, Field
class UserIn(BaseModel):
model_config = ConfigDict(
extra="forbid", # 禁止多余字段
str_strip=True, # 去除字符串空白
populate_by_name=True, # 允许使用字段名
)
username: str = Field(..., alias="userName")
age: int = Field(..., ge=0)
# 序列化示例
user = UserIn(userName=" alice ", age=30)
print(user.model_dump()) # {'username': 'alice', 'age': 30}
print(user.model_dump_json(exclude={"age"})) # {"username":"alice"}
实战场景:在前后端分离项目中,前端使用驼峰命名(如userName),后端使用蛇形命名(如username)。通过alias和populate_by_name=True,我们既能让API接收驼峰字段,又能在Python代码中使用蛇形属性。同时,extra="forbid"能有效防止客户端传入未定义字段,减少安全风险。
常见误区:在响应模型中滥用extra="forbid"。若响应模型包含额外字段,FastAPI会抛出ResponseValidationError,导致500错误。建议响应模型使用默认的extra="ignore",仅对请求模型启用"forbid"。
总结复盘:Pydantic模型是FastAPI数据层的基石。掌握字段类型、必填与默认值、嵌套模型、自定义验证器以及模型配置,意味着你能够精确控制API的输入输出契约。在实际项目中,建议为每个核心实体定义独立的Pydantic模型,并区分请求模型(严格验证)与响应模型(最小化暴露)。同时,善用model_dump()的exclude、include参数,避免将敏感字段(如密码哈希)序列化到响应中。最后,定期审查验证器的性能,避免在热路径上执行重IO操作。