(四)从类型注解走到 Pydantic 校验
1. 观察模型创建过程
上一章的类型注解可以提示编辑器,却不会自动检查所有输入。Pydantic 的 BaseModel 把“检查输入并构造对象”连成一个实际动作。先运行一个短例:
# 项目文件:demos/body_model.py
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
class Draft(BaseModel):
title: str = Field(min_length=1, max_length=100)
finished: bool = False
app = FastAPI()
@app.post("/books", response_model=Draft, status_code=201)
def add_book(data: Draft):
# data 已经是模型对象;点号读取经过校验的字段。
return data
if __name__ == "__main__":
with TestClient(app) as client:
for payload in [
{"title": "Python Web"},
{"title": "A", "finished": "false"},
{"title": 12},
]:
response = client.post("/books", json=payload)
print(response.status_code, response.json())# TestClient 在本进程内把请求交给 ASGI 应用,不另外监听端口。
uv run python ../demos/body_model.py
第一条只提交书名,得到 201,finished 采用 False。第二条提交字符串 "false",这个宽松模型把它转成布尔值 False。第三条把书名写成数字,得到 422,位置指向 body 中的 title。
这里没有执行 Draft(**data) 的手写路由代码,FastAPI 根据 data: Draft 执行了这一步。模型失败时函数体不执行;但请求的不同字段和依赖由框架一起解析,身份错误与字段错误同时存在时,要根据实际响应定位,不能把整个解析过程想成一条固定的逐项队列。
2. 按书架规则建立输入模型
书名去掉两端空白后保留 1 至 100 个字符;标签是最多 10 项的文本列表,每项去空白后保留 1 至 30 个字符;同名标签保留第一次。finished 接收 JSON 布尔值。字段拼错和附加的 owner_id 都交回错误。
# 项目文件:api_project/src/bookapi/schemas.py
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
# Annotated 保留 str 的含义,再附上 Pydantic 应执行的规则。
Title = Annotated[str, Field(strict=True, min_length=1, max_length=100)]
Tag = Annotated[str, Field(strict=True, min_length=1, max_length=30)]
Tags = Annotated[list[Tag], Field(max_length=10)]
Flag = Annotated[bool, Field(strict=True)]
class InputModel(BaseModel):
# 拒绝拼错字段和客户端偷偷附加的 owner_id。
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
class BookCreate(InputModel):
title: Title
finished: Flag = False
tags: Tags = Field(default_factory=list)
@field_validator("tags")
@classmethod
def unique_tags(cls, value: list[str]) -> list[str]:
# 保留标签首次出现的顺序;此时每项已经完成类型和长度校验。
return list(dict.fromkeys(value))
class BookOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
title: str
finished: bool
tags: list[str]
class BookPage(BaseModel):
total: int
offset: int
limit: int
items: list[BookOut]Field(strict=True) 让 Flag 不再把 1 或 "false" 转成布尔值。ConfigDict 设置模型行为;extra="forbid" 让额外字段成为错误,str_strip_whitespace=True 对文本执行去空白。InputModel 的子类继承这些设置。
default_factory=list 在每次创建模型时给出一份列表。@field_validator("tags") 默认在该字段的基本校验之后运行;函数收到的已经是合格的字符串列表。cls 对应当前模型类,装饰器按类方法方式调用它。dict.fromkeys 按首次出现的顺序取键,最后转回列表。
用 {"title":" Python Web ","tags":[" 网络 ","网络"]} 观察变化:书名先去空白成为 "Python Web";两个标签分别去空白成为同名文本;字段校验器再合并成 ["网络"]。输入 {"title":"A","finished":"false"} 则在布尔检查处失败,得不到 BookCreate。
BookCreate.model_validate(mapping) 接收字典并执行同样的检查;book.model_dump() 返回 Python 字典。模型内部的字段仍然是属性,可以写 book.title。需要交给 JSON 时,model_dump(mode="json") 会把日期等类型也转成 JSON 能表达的形式。这章的文本、布尔和列表两种模式结果相同。
模型方法与配置可以继续查阅 Pydantic 模型文档;校验器的位置与执行方式见 校验器文档。