第 5 章 · 参数校验进阶与 Annotated
本章目标:掌握官方推荐的 Annotated 声明式校验写法,熟练使用数值/长度/正则约束与自定义校验器,并能读懂 422 错误响应。
5.1 Annotated:官方推荐写法
从 0.95.0 版本起,FastAPI 官方推荐用 Python 标准库的 Annotated 声明参数元数据:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
def read_items(
q: Annotated[str | None, Query(max_length=50)] = None,
# 类型 ─────────────┘ └── 校验元数据
):
# GET /items/?q=hello 正常;q 超过 50 字符返回 422
return {"q": q}结构是 Annotated[实际类型, 元数据1, 元数据2, ...],默认值仍写在参数的 = 后面。对比旧写法:
# ❌ 旧写法(0.95.0 之前):Query 占据了默认值的位置
def read_items(q: str | None = Query(default=None, max_length=50)): ...
# ✅ 新写法:默认值回归默认值,校验信息放进 Annotated
def read_items(q: Annotated[str | None, Query(max_length=50)] = None): ...官方给出的 Annotated 优势:
- 默认值语义不被污染:直接调用这个函数(不经 FastAPI)时,参数就是普通参数,不会拿到
QueryInfo之类的对象; - 可复用:
Annotated类型可以抽成别名在多个接口间共享; - 兼容其他工具:同一函数还能被 Typer 等同样基于 Annotated 的库使用。
Annotated 里不能用 Query(default=...)
Annotated[str, Query(default="rick")] = "morty" 会报错。在 Annotated 内部,Query() 不接受 default 参数——默认值只写在参数 = 后面。
5.2 字符串约束:长度与正则
Query/Path/Body/Field 共享同一套约束参数:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search/")
def search(
q: Annotated[
str,
Query(
min_length=3, # 最少 3 个字符
max_length=50, # 最多 50 个字符
pattern=r"^[\w\u4e00-\u9fa5 ]+$", # 只允许字母数字下划线中文空格
title="搜索关键词",
description="长度 3-50,仅限中英文、数字与空格",
),
],
):
return {"q": q}pattern 使用正则约束格式,比如强制 ^fixedquery$ 就是精确匹配。title/description 不参与校验,但会展示在 OpenAPI 文档里。
5.3 数值约束:gt / ge / lt / le
路径参数同样可以校验,用 Path:
from typing import Annotated
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: Annotated[int, Path(title="商品 ID", gt=0, lt=10000)],
size: Annotated[float, Query(ge=0.1, le=10)] = 1.0,
):
# item_id 必须是 1..9999 的整数(gt=0 排除 0 和负数)
# size 必须在 0.1 到 10 之间(含边界,ge/le 是大于等于/小于等于)
return {"item_id": item_id, "size": size}四个缩写的含义务必分清:gt=greater than(严格大于)、ge=greater or equal、lt=less than、le=less or equal。想表达"必须为正数但可以小于 1"只能用 gt=0,ge=1 会错误地排除 0.5。
5.4 Pydantic 模型字段的校验:Field
请求体字段用 Field,参数名与 Query/Path 完全一致:
from pydantic import BaseModel, Field
from fastapi import FastAPI
app = FastAPI()
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100, examples=["机械键盘"])
price: float = Field(gt=0, description="单价,必须大于 0")
stock: int = Field(default=0, ge=0)
sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$") # 形如 ABC-1234
@app.post("/products/")
def create(p: Product):
return p.model_dump()5.5 自定义校验器
内置约束不够用时,Pydantic v2 提供 field_validator(单字段)和 model_validator(整体):
from pydantic import BaseModel, field_validator, model_validator
class Registration(BaseModel):
username: str
password: str
password_confirm: str
@field_validator("username")
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("用户名只能包含字母和数字")
return v.lower() # 可以顺便做规范化转换
@model_validator(mode="after")
def passwords_match(self) -> "Registration":
# mode="after":所有字段校验通过后执行,可访问 self 的全部字段
if self.password != self.password_confirm:
raise ValueError("两次输入的密码不一致")
return self校验失败抛出的 ValueError 会被 Pydantic 捕获并进入 422 响应的 detail 列表,用户看到的是友好错误而非堆栈。
5.6 读懂 422 错误响应
一次失败请求的响应体:
{
"detail": [
{
"type": "greater_than",
"loc": ["body", "price"],
"msg": "Input should be greater than 0",
"input": "-5",
"ctx": { "gt": 0 }
}
]
}- loc:错误位置数组。
["path","item_id"]是路径参数,["query","q"]是查询参数,["body","price"]是请求体字段,嵌套模型会给出完整路径如["body","items",0,"url"]; - type:机器可读的错误类型(int_parsing、string_too_long、greater_than…);
- ctx:错误上下文(违反的约束值)。
前端可以按 loc 精确定位到表单控件——这是 FastAPI 校验体系对全栈体验的最大贡献。
5.7 本章小结
- 新代码统一用
Annotated[类型, Query(...)]写法,默认值回归=;Annotated 内不能再用default=; - 字符串约束
min_length/max_length/pattern,数值约束gt/ge/lt/le,Query/Path/Body/Field 通用; - 自定义校验用
field_validator(单字段)与model_validator(mode="after")(跨字段); - 422 响应的
loc/type/msg/ctx能精确定位每个错误,嵌套模型也不例外。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 关于 Annotated 写法,下列哪句是正确的?
2. 要求参数必须大于 0 且可以取 0.5,应该用哪个约束?
3. 422 响应中 loc 为 ["body", "items", 0, "url"] 表示什么?
4. 想校验"两个字段必须相等"这类跨字段规则,应该用?
🛠️ 动手实践
- 给第 4 章的 Product 模型补全约束:name 1–50 字符、price > 0 且 < 100000、sku 正则
^[A-Z]{3}-\d{4}$,用 curl 验证三种非法输入的 422 报错。 - 写一个
field_validator把用户输入的手机号规范化:去掉+86前缀和所有连字符,最终必须是 11 位数字,否则抛 ValueError。 - 实现
GET /prices/{amount},要求 amount 是Annotated[float, Path(gt=0)],测试?amount=0与/prices/-1分别得到什么错误。
校验体系已经完整,进入第 6 章控制响应的形状。