第 3 章 · Pydantic v2 模型与数据校验
本章目标:掌握 Pydantic v2 的 BaseModel、Field 字段约束与 field_validator / model_validator;理解模型配置(model_config)、序列化与反序列化;实现嵌套模型表达商品-分类-规格等复杂结构;在 svc-demo 中用 Pydantic 替代 ch02 的字典假数据,为 ch04 请求绑定与 ch05 数据库映射打基础;能对照 Django Serializer 与 Flask Marshmallow 说明差异。
学时建议:4~5 小时(含 1.5 小时跟练)
前置:完成 fastapi-web ch02;python-dev 中熟悉类型提示(list[str]、Optional)更佳。
3.1 场景说明:从 dict 到强类型模型
ch02 的 _FAKE_PRODUCTS 使用裸字典,存在隐患:
| 问题 | dict 写法 | Pydantic 写法 |
|---|---|---|
| 字段拼写错误 | "nmae" 不报错 | 校验失败,422 响应 |
| 价格负数 | 无约束 | Field(ge=0) |
| 嵌套分类 | 手写嵌套 dict | CategoryRead 嵌套模型 |
| API 文档 | 无字段说明 | 自动生成 schema |
| ORM 转换 | 手动 dict() | model_validate / from_attributes |
svc-demo 对外契约由 Pydantic 模型驱动,与 api.example.com 前端/SDK 对齐。
数据流(svc-demo)
JSON 请求体 → Pydantic 解析校验 → 业务逻辑 → Pydantic 响应 → JSON
3.2 Pydantic v2 与 v1 关键差异
| 特性 | Pydantic v1 | Pydantic v2(本章) |
|---|---|---|
| 校验装饰器 | @validator | @field_validator |
| 模型级校验 | @root_validator | @model_validator |
| 配置 | class Config | model_config = ConfigDict(...) |
| ORM 模式 | orm_mode = True | from_attributes = True |
| 性能 | 较慢 | Rust 核心 pydantic-core,更快 |
FastAPI 0.100+ 默认绑定 Pydantic v2,本章全部使用 v2 语法。
3.3 BaseModel 基础
3.3.1 商品读取模型 app/schemas/product.py
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, ConfigDict, Field
class ProductBase(BaseModel):
sku: str = Field(..., min_length=3, max_length=32, examples=["SKU-001"])
name: str = Field(..., min_length=1, max_length=128)
price: Decimal = Field(..., ge=0, decimal_places=2, description="售价(元)")
stock: int = Field(default=0, ge=0)
class ProductCreate(ProductBase):
"""创建商品时的入参(无 id)"""
category_id: int = Field(..., gt=0)
class ProductRead(ProductBase):
"""对外响应(含 id、时间戳)"""
model_config = ConfigDict(from_attributes=True)
id: int
is_active: bool = True
created_at: datetime | None = None
3.3.2 独立脚本验证
item = ProductCreate(sku="SKU-100", name="演示商品", price=Decimal("19.99"), category_id=1)
print(item.model_dump_json()) # 合法
# ProductCreate(sku="AB", price=Decimal("-1"), ...) # 触发 ValidationError
3.4 Field 详解
from pydantic import BaseModel, Field
class SearchQuery(BaseModel):
q: str = Field(
...,
min_length=1,
max_length=64,
description="搜索关键词",
json_schema_extra={"example": "键盘"},
)
page: int = Field(1, ge=1, description="页码,从 1 开始")
page_size: int = Field(20, ge=1, le=100, alias="pageSize")
model_config = ConfigDict(populate_by_name=True) # 同时接受 pageSize 与 page_size
| Field 参数 | 含义 | 示例 |
|---|---|---|
... | 必填 | name: str = Field(...) |
default / default_factory | 默认值 | tags: list[str] = Field(default_factory=list) |
ge / le / gt / lt | 数值边界 | price: Decimal = Field(ge=0) |
min_length / max_length | 字符串长度 | sku: str = Field(min_length=3) |
pattern | 正则 | sku: str = Field(pattern=r"^SKU-\d{3}$") |
description | OpenAPI 文档说明 | 出现在 /docs |
alias | 外部字段名 | JSON 用 pageSize,Python 用 page_size |
3.5 field_validator 字段校验器
Pydantic v2 使用 @field_validator,需指定 mode:
from pydantic import BaseModel, field_validator, ValidationError
class ProductCreate(BaseModel):
sku: str
name: str
price: float
@field_validator("sku")
@classmethod
def sku_must_uppercase(cls, v: str) -> str:
return v.strip().upper()
@field_validator("name")
@classmethod
def name_not_blank(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("商品名称不能为空")
return v
@field_validator("price")
@classmethod
def price_reasonable(cls, v: float) -> float:
if v > 999_999:
raise ValueError("价格超出允许范围")
return round(v, 2)
| mode | 时机 | 典型用途 |
|---|---|---|
before(默认) | 类型转换前 | 清洗字符串、预处理 |
after | 类型转换后 | 业务规则校验 |
多个字段联合校验用 model_validator:
from pydantic import BaseModel, model_validator
class PriceTier(BaseModel):
retail_price: float
wholesale_price: float
@model_validator(mode="after")
def wholesale_cheaper_than_retail(self):
if self.wholesale_price > self.retail_price:
raise ValueError("批发价不能高于零售价")
return self