FastAPI 查询参数与字符串校验(Query 参数)完整指南:从 `Query()` 到 `AfterValidator`
本文围绕 FastAPI 官方教程"Query 参数与字符串校验"(对应仓库文档 docs/fr/docs/tutorial/query-params-str-validations.md)展开,讲解如何在查询参数上声明额外的校验与元数据:长度约束、正则表达式、默认值、必填性、多值列表、别名、弃用标记,以及基于 Pydantic
AfterValidator的自定义校验。读完你将掌握基于Annotated的现代写法,并能在交互式 API 文档与 OpenAPI 模式中看到每一处声明的落地效果。
一、从最小的查询参数开始:为什么需要额外校验
FastAPI 允许开发者对路径操作函数中的参数声明额外的信息与校验规则,其底层依赖 Python 类型注解与 Pydantic 的字段校验能力。先看仓库中的最简起点示例 docs_src/query_params_str_validations/tutorial001_py310.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(q: str | None = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
这里 q 是唯一的查询参数,类型为 str | None,默认值为 None。
关于该声明的两个关键结论:
= None让参数可选:FastAPI 依据默认值判断参数是否必需。只要存在默认值None,参数就不是必填项。str | None带来编辑器收益:联合类型可以让 IDE 与静态类型检查工具提供更好的补全与错误提示(例如避免在可能为None的值上直接调用字符串方法)。
但仅靠类型注解,我们尚无法表达"如果传了 q,它的长度不能超过 50 个字符"这类约束。这就需要引入 Query。
二、引入 Query 与 Annotated:声明式校验的基石
要实现额外校验,需要导入两个符号:
Query:来自fastapi,是声明查询参数校验与元数据的核心工具;Annotated:来自标准库typing,用于把元数据"附加"到类型注解上。
对应代码见 docs_src/query_params_str_validations/tutorial002_an_py310.py:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(max_length=50)] = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
版本前提:FastAPI 从 0.95.0 版本起支持并推荐
Annotated写法。若仓库版本过旧,使用Annotated会出现导入或类型解析错误,请先按 docs/en/docs/deployment/versions.md 中的升级指引将 FastAPI 升级到至少 0.95.1。
2.1 把 Annotated 包在类型外层
在 Python 类型教程中我们已知 Annotated 可用于在类型上附加元数据(即 PEP 593 的 Type Hints with Metadata Annotations)。于是:
q: str | None = None
可以改写为:
q: Annotated[str | None] = None
两者语义完全相同——q 的类型仍是 str | None,默认值仍是 None。区别在于 Annotated 提供了"第二槽位"来放置 FastAPI 能读取的额外信息。
2.2 把 Query(max_length=50) 放进 Annotated
将 Query 放入 Annotated 的元数据槽位,并传入 max_length=50:
q: Annotated[str | None, Query(max_length=50)] = None
注意这里函数参数的默认值仍是 None,所以 q 依旧可选;但 Query(max_length=50) 告诉 FastAPI:一旦该参数被提供,其值最多 50 个字符。
关于
Query()的适用范围:本教程用的是Query(),因为它作用于查询参数。FastAPI 还有Path()、Body()、Header()、Cookie()等同类工具,它们接受与Query()相同的参数集合,只是作用的参数来源不同,后续章节会逐一展开。
启用上述声明后,FastAPI 会自动完成三件事:
- 校验数据——确保长度不超过 50 字符;
- 给出清晰错误——当数据非法时向客户端返回结构化的 422 校验错误;
- 写入 OpenAPI 模式——参数会被记录进 OpenAPI 的
paths定义中,从而自动出现在/docs与/redoc交互式文档里。
三、旧式写法:把 Query 当作函数默认值(了解即可)
在 0.95.0 之前(即 2023 年 3 月前),FastAPI 要求把 Query() 直接放在函数参数的默认值位置,而不是放进 Annotated。老代码库中仍大量存在这种写法,官方文档用一节专门解释它,见 docs_src/query_params_str_validations/tutorial002_py310.py:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: str | None = Query(default=None, max_length=50)):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
由于无法使用 Annotated,就必须用 Query(default=None) 顶替原本的默认值 None:
q: str | None = Query(default=None)
它与下面这句对于 FastAPI 而言等价(参数可选、默认 None):
q: str | None = None
差别在于前者显式声明了这是一个查询参数。在此基础上叠加 max_length 等字符串校验:
q: str | None = Query(default=None, max_length=50)
3.1 两种写法不可混用:Query 内部的 default
使用 Annotated 时,不能再给 Query 传 default 参数,因为真正的默认值应该写在函数参数上,否则会产生歧义。例如下面这种写法不被允许:
q: Annotated[str, Query(default="rick")] = "morty"
——因为 FastAPI 无法判断默认值到底是 "rick" 还是 "morty"。正确做法是二选一:
# 推荐:默认值写在函数参数上
q: Annotated[str, Query()] = "rick"
# 老式代码库常见
q: str = Query(default="rick")
3.2 为什么官方推荐 Annotated
官方在文档中明确指出:优先使用 Annotated,它在新代码中"有诸多好处、没有缺点",理由包括:
- 默认值更符合 Python 直觉:函数参数上的默认值就是"真正的默认值",与其他普通 Python 函数一致;
- 函数可脱离 FastAPI 复用:直接把同一函数当作普通函数调用即可。若某参数必填(无默认值),编辑器与 Python 解释器都会在漏传时立即报错;
- 旧式写法有隐蔽陷阱:当函数在没有 FastAPI 的场景下被调用时,若忘记传入参数,拿到的会是
QueryInfo之类的包装对象而不是真正的str,且编辑器与解释器都不会提前报错,直到内部运算失败才暴露问题; - 元数据可叠加:
Annotated支持多个元数据注解,同一签名还能被 Typer 等其他工具复用。
四、叠加更多校验:min_length 与正则 pattern
4.1 同时限制最小与最大长度
长度约束可以组合使用。示例见 docs_src/query_params_str_validations/tutorial003_an_py310.py:
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
):
...
这样 q 一旦被传入,长度必须落在 [3, 50] 区间内。
4.2 用正则表达式限定取值形态
Query 还接受 pattern 参数,要求参数值必须匹配指定的正则表达式。示例见 docs_src/query_params_str_validations/tutorial004_an_py310.py:
@app.get("/items/")
async def read_items(
q: Annotated[
str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$")
] = None,
):
...
以 "^fixedquery$" 为例,逐段解释其含义:
^:锚定字符串开头,表示fixedquery之前不允许有任何字符;fixedquery:要求中间这段必须恰好等于fixedquery;$:锚定字符串结尾,表示fixedquery之后不允许再有字符。
正则表达式对不少开发者来说是个难点,不必焦虑——没有正则你也能完成大量工作,只需知道在 FastAPI 中需要时可以随时使用即可。
五、默认值与非 None 场景
默认值不限于 None。假设希望 q 的默认值是 "fixedquery",同时要求最小长度 3,可参考 docs_src/query_params_str_validations/tutorial005_an_py310.py:
@app.get("/items/")
async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"):
...
注意此时类型注解已退化为单纯的 str(不再有 | None),因为默认值是具体字符串。
通用规则:只要参数带默认值(无论默认值是
None还是其他类型),该参数就是**可选(非必填)**的。
六、必填查询参数的各种姿势
6.1 普通必填参数
若不需要额外校验或元数据,直接不给默认值即可:
q: str
而不是:
q: str | None = None
但如果还需要 Query,则同样只需省略默认值,见 docs_src/query_params_str_validations/tutorial006_an_py310.py:
@app.get("/items/")
async def read_items(q: Annotated[str, Query(min_length=3)]):
...
请求 URL 未携带 q 时,FastAPI 会返回 422 校验错误并说明该参数缺失。
6.2 "必填但可为 None"
还有一种边界情况:参数必须被客户端显式发送,但允许其值为 None。做法是保留 None 为合法类型、同时不写默认值,见 docs_src/query_params_str_validations/tutorial006c_an_py310.py:
@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(min_length=3)]):
...
与 6.1 的区别仅在于类型是否包含 None:这里客户端必须发送 q 参数(即使发送 ?q= 这类空值),否则仍会判定为缺失。
七、多值查询参数:接收列表
7.1 同一参数名出现多次
只要显式使用 Query,就可以把参数声明为接收多个值。例如 URL 中 q 可以出现多次,见 docs_src/query_params_str_validations/tutorial011_an_py310.py:
@app.get("/items/")
async def read_items(q: Annotated[list[str] | None, Query()] = None):
query_items = {"q": q}
return query_items
访问:
http://localhost:8000/items/?q=foo&q=bar
q 参数会以 Python list 形式进入函数,响应为:
{
"q": [
"foo",
"bar"
]
}
为什么必须显式写
Query():若不写,list类型的参数会被 FastAPI 误判为请求体的一部分,而非查询参数。
文档示意图展示了交互式文档中该参数会升级为"允许多个值输入"的控件:
7.2 为多值列表提供默认值
未提供任何值时返回默认列表,见 docs_src/query_params_str_validations/tutorial012_an_py310.py:
@app.get("/items/")
async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]):
query_items = {"q": q}
return query_items
直接访问 http://localhost:8000/items/(不带任何查询参数),q 取默认值,响应同样为:
{
"q": [
"foo",
"bar"
]
}
7.3 直接使用裸 list 的代价
也可以只写 list 而不写 list[str],见 docs_src/query_params_str_validations/tutorial013_an_py310.py:
@app.get("/items/")
async def read_items(q: Annotated[list, Query()] = []):
...
注意差异:
list[int]会校验并文档化"列表内容必须是整数";而裸list不会校验元素类型。因此建议尽量给出元素类型,以获得完整校验与 OpenAPI 文档。
八、参数元数据:title、description、alias、deprecated
下面的元数据声明都会进入生成的 OpenAPI 模式,被自动文档与外部代码生成等工具消费。需要说明的是,不同工具对 OpenAPI 的支持程度可能不同,个别字段未必全部展示,但这不影响其在模式中的存在。
8.1 title 与 description
增加人类可读的标题,见 docs_src/query_params_str_validations/tutorial007_an_py310.py:
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(title="Query string", min_length=3)] = None,
):
...
再叠加长文本描述,见 docs_src/query_params_str_validations/tutorial008_an_py310.py:
q: Annotated[
str | None,
Query(
title="Query string",
description="Query string for the items to search in the database that have a good match",
min_length=3,
),
] = None
8.2 alias:把参数映射到非法 Python 标识符
若希望 URL 中使用的参数名是 item-query(连字符使其不是合法 Python 变量名),最接近的变量名只能是 item_query。此时用 alias 声明"URL 中的真实名称",见 docs_src/query_params_str_validations/tutorial009_an_py310.py:
@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None):
...
访问:
http://127.0.0.1:8000/items/?item-query=foobaritems
FastAPI 就会用别名 item-query 去 URL 中取值,并注入到参数 q。
8.3 deprecated=True:标记参数已弃用
当某个参数仍需暂时保留以兼容存量客户端、但希望文档明确提示其"已弃用"时,见 docs_src/query_params_str_validations/tutorial010_an_py310.py:
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(alias="item-query")] = None,
query: Annotated[
str | None,
Query(
alias="item-query",
title="Query string",
description="Query string for the items to search in the database that have a good match",
min_length=3,
max_length=50,
pattern="^fixedquery$",
deprecated=True,
),
] = None,
):
...
带 deprecated=True 的参数在文档界面中会被突出标记为弃用:
8.4 include_in_schema=False:从 OpenAPI 中排除参数
若想让某个查询参数完全不出现在 OpenAPI 与自动文档中(例如仅供内部调试),设置 include_in_schema=False,见 docs_src/query_params_str_validations/tutorial014_an_py310.py:
@app.get("/items/")
async def read_items(
hidden_query: Annotated[str | None, Query(include_in_schema=False)] = None,
):
if hidden_query:
return {"hidden_query": hidden_query}
else:
return {"hidden_query": "Not found"}
该参数仍可正常接收与使用,只是不会出现在 /docs 等文档与导出的 OpenAPI 模式中。
九、Query 底层实现速览
从源码 fastapi/param_functions.py 可以确认 Query() 的完整参数体系,除本教程用到的之外还包括 gt/ge/lt/le(数字校验)、alias_priority、validation_alias、serialization_alias 等。结合定义与官方文档可得如下速查表:
| 类别 | 参数 | 作用 | 适用类型 |
|---|---|---|---|
| 基础 | default |
参数默认值(用 Annotated 时禁止使用) |
任意 |
| 基础 | default_factory |
惰性生成默认值的可调用对象 | 任意 |
| 字符串 | min_length |
最小长度 | str |
| 字符串 | max_length |
最大长度 | str |
| 字符串 | pattern |
必须匹配的正则表达式 | str |
| 数字 | gt/ge/lt/le |
大于/大于等于/小于/小于等于 | 数值 |
| 元数据 | title / description |
供文档与外部工具展示的标题/描述 | 任意 |
| 元数据 | alias |
用于取值与 OpenAPI 的替代名称 | 任意 |
| 元数据 | deprecated |
在文档中标记为已弃用 | 任意 |
| 元数据 | include_in_schema |
为 False 时从 OpenAPI 中排除 |
任意 |
实现层面,Query() 会构造一个 params.Query 描述对象,最终由 FastAPI 的依赖解析与 Pydantic 校验管道在请求到达时执行长度、正则与类型校验。类似的 Path()、Body()、Header()、Cookie() 等函数定义也在同一文件(fastapi/param_functions.py)中,共享几乎一致的校验参数集——这也解释了为什么本章学到的 min_length、pattern、alias 等写法在后续章节可以直接迁移到路径参数、请求体字段等场景。
十、自定义校验:Pydantic AfterValidator
当内置校验参数不足以表达业务规则时,FastAPI 支持挂载自定义校验函数。它会在常规校验(例如"值确实是 str")之后执行,入口是 Pydantic v2 提供的 AfterValidator,同样放进 Annotated。Pydantic 还提供 BeforeValidator 等其他变体。
版本要求:
AfterValidator需要 Pydantic 2 及以上版本。FastAPI 仓库默认基于 Pydantic v2(FastAPI 0.100.0+ 起),其 pydantic v1 兼容路径独立维护在 docs_src/pydantic_v1_in_v2 目录中。
10.1 完整示例:ISBN / IMDB 前缀校验
下面来自 docs_src/query_params_str_validations/tutorial015_an_py310.py 的例子演示了如何校验一个 item ID 必须以前缀 isbn-(图书 ISBN)或 imdb-(电影 IMDB 编号)开头:
import random
from typing import Annotated
from fastapi import FastAPI
from pydantic import AfterValidator
app = FastAPI()
data = {
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}
def check_valid_id(id: str):
if not id.startswith(("isbn-", "imdb-")):
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
return id
@app.get("/items/")
async def read_items(
id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
if id:
item = data.get(id)
else:
id, item = random.choice(list(data.items()))
return {"id": id, "name": item}
核心就一句话:把函数放进 AfterValidator(...),再连同 AfterValidator 一起放进 Annotated。函数签名接收待校验值,不满足时抛出 ValueError,满足则原样(或转换后)返回该值。若用户传了非法前缀,如 ?id=bogus-id,会收到校验错误;若不传 id,则会从 data 字典中随机返回一条记录作为演示建议。
10.2 逐行理解这段代码
如果只关心 FastAPI 的用法,可以跳过本小节;若想厘清代码细节:
str.startswith()支持传入元组,会依次检查元组中的每个前缀(例如("isbn-", "imdb-"));data.items()返回一个由(键, 值)元组组成的可迭代对象,list(data.items())将其转成普通列表;random.choice()从列表中随机取出一个(id, name)元组,形如("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy");- Python 的元组解包把这两个值分别赋给变量
id和name,因此即便用户不提供 ID,也能得到一条随机的建议记录——所有这些只用了一行代码。
10.3 什么时候不该用自定义校验器
如果校验逻辑需要与外部组件(数据库、其他 API)通信,则应改用 FastAPI 依赖注入(Dependencies) 机制,而不是 AfterValidator。AfterValidator 只适合处理仅凭请求中已有数据即可完成的检查。
十一、小结:一张速查表
本章覆盖的校验与元数据可归为三类:
- 通用元数据:
alias、title、description、deprecated(另可加include_in_schema); - 字符串专属校验:
min_length、max_length、pattern; - 自定义校验:借助 Pydantic
AfterValidator挂载自写校验函数。
这些能力都以 Query(...) 为入口、以 Annotated[类型, Query(...)] 为推荐载体。官方给出的泛化规则同样适用于后续章节中的 Path()、Body()、Header()、Cookie()。本章所有示例代码位于仓库 docs_src/query_params_str_validations 目录,可直接运行验证;下一章将介绍如何为数字等非字符串类型声明类似校验(gt、ge、lt、le)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

