FastAPI 查询参数(Query)声明与字符串校验完整实战指南:Annotated、Validation 与 OpenAPI 元数据
导读:本文将基于 FastAPI 官方教程中「Query Parameters and String Validations」一节的完整内容(对应中文环境下的教程文档 docs/hi/docs/tutorial/query-params-str-validations.md),系统讲解如何在路径操作函数中为查询参数声明额外校验规则与元数据——从最基础的可选参数写法,到 Annotated + Query 的推荐用法、字符串的长度/正则校验、必填参数、多值列表,再到 alias、deprecated、include_in_schema 及基于 Pydantic AfterValidator 的自定义校验。读完本文,你将掌握一套可直接复制运行的查询参数声明方案,并能结合 FastAPI 源码理解这些校验与 OpenAPI 文档化背后的实现机制。
一个最基础的可选查询参数
FastAPI 允许你为参数声明额外的信息和校验规则。先看一个最简单的应用:
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,表示它类型为 str,但也可能是 None;实际上它的默认值确实是 None,因此 FastAPI 会知道它不是必填的。
注:FastAPI 之所以判定
q不是必填,正是因为它带有默认值= None。 使用str | None(而非裸str)会让你的编辑器提供更好的类型提示支持,并在编写阶段就帮助检测错误。
完整代码可在 tutorial001_py310.py 中查看。
附加校验:让 q 的长度不超过 50
现在我们要强制:即便 q 是可选的,一旦它被提供,其长度不能超过 50 个字符。这是一类典型的字符串约束场景(例如控制日志关键词、搜索串的长度)。
导入 Query 与 Annotated
要做到这一点,先完成两个导入:
- 从
fastapi导入Query - 从
typing导入Annotated
from typing import Annotated
from fastapi import FastAPI, Query
注:FastAPI 在 0.95.0 版本中加入了对
Annotated的支持(并开始推荐使用它)。如果你使用的是更旧的版本,尝试使用Annotated会遇到错误。请先确保将 FastAPI 版本至少升级到 0.95.1 再使用Annotated。
在类型标注中使用 Annotated
回顾类型提示基础部分:Annotated 可用于给参数添加元数据。现在正是它在 FastAPI 中发挥作用的时候。
之前我们写的是:
q: str | None = None
把它用 Annotated 包一层,变成:
q: Annotated[str | None] = None
这两种写法的含义相同:q 是一个可以为 str 也可以为 None 的参数,且默认值为 None。而 Annotated 的额外价值在于——它提供了一个可以继续"塞入更多信息"的载体。
把 Query 放进 Annotated
现在,在 Annotated 内部加入 Query,并把 max_length 设为 50:
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
注意:默认值仍然是 None,所以参数依旧可选。但 Annotated 内部的 Query(max_length=50) 明确告诉 FastAPI:我们希望对这个值施加附加校验——最多 50 个字符。
提示:这里之所以使用
Query(),是因为它是一个查询参数(query parameter)。后续教程中还会看到Path()、Body()、Header()、Cookie()等,它们接受与Query()几乎一致的参数。
声明之后,FastAPI 会自动做三件事:
- 校验(Validate)数据——确保长度不超过 50 个字符;
- 数据不合法时,向客户端返回清晰的错误信息(422 Validation Error);
- 在 OpenAPI schema 的 path operation 中文档化该参数,使其显示在自动生成的交互式文档 UI 中。
从源码看,Query 是 fastapi/params.py 中 Param 的子类(fastapi/params.py#L221),其类属性 in_ = ParamTypes.query 将参数定位为 query 位置;而整个 Param 类则继承自 Pydantic 的 FieldInfo(fastapi/params.py#L26),因此 min_length、max_length、pattern 这些约束最终都交由 Pydantic 字段校验系统落地,这也解释了为什么约束失败时返回的 422 错误结构与 Pydantic 校验错误一致。ParamTypes 枚举(query、header、path、cookie)见 fastapi/params.py#L19-L23。
旧式写法(已不推荐):把 Query 当作默认值
FastAPI 在 0.95.0 之前(即 2023 年 3 月之前)的版本里,你必须把 Query 用作函数参数的默认值,而不是放进 Annotated。由于存量代码中仍大量存在这种写法,这里做必要讲解。
这种写法下,因为不能把 None 作为默认值直接保留,你需要这样写:
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) 把默认值显式声明出来,这在 FastAPI 语义上与 q: str | None = None 等价——区别在于 Query 版本更明确地把它声明为查询参数。
再进一步,还可以给 Query 传入更多参数,本例中的 max_length 就是针对字符串生效的:
q: str | None = Query(default=None, max_length=50)
它同样会校验数据、在数据非法时返回清晰错误,并把参数写进 OpenAPI schema。
提示:对于新代码,只要条件允许,都应优先使用前文介绍的
Annotated写法——它好处很多且没有坏处。
Query 放在默认值还是 Annotated 中?
需要注意:在 Annotated 中使用 Query 时,不能再给 Query 传 default 参数,而应使用函数参数本身的默认值,否则语义会不一致。
以下写法是不允许的:
q: Annotated[str, Query(default="rick")] = "morty"
因为这里完全无法判断默认值到底是 "rick" 还是 "morty"。
所以你应该(优先)这样写:
q: Annotated[str, Query()] = "rick"
而在旧代码库中,你更可能见到的是:
q: str = Query(default="rick")
使用 Annotated 的优势
官方强烈推荐优先使用 Annotated 而非把 Query 放在默认值位置,主要理由如下:
- 函数参数的真实默认值就是逻辑上的默认值——这与 Python 本身的直觉一致;
- 你可以在没有 FastAPI 的场景下照常调用这个函数,它仍按预期工作:如果存在必填参数(没有默认值),编辑器会直接报错,Python 在运行时也会因为缺少必填参数而抛出
TypeError; - 反观旧式的"默认值即
Query()"写法:脱离 FastAPI 调用时,你必须记得传入参数,否则拿到的是QueryInfo之类的对象而非普通str,且编辑器与 Python 都不会在调用处报错,只有内部运算出错时才会暴露问题; - 由于
Annotated可以承载多个元数据注解,同一个函数还能与其他工具(如 Typer 这类 CLI 框架)共用同一套类型信息,天然具备更强的可组合性。
添加更多字符串校验
min_length:最小长度
在 Query 中加入 min_length,即可同时约束最小长度:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
正则表达式 pattern
你还可以定义一个参数必须匹配的正则表达式(regex) pattern:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[
str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$")
] = None,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
这里 "^fixedquery$" 这个正则的含义可拆解为:
^:值以紧随其后的字符开头,前面不允许再有其他字符;fixedquery:必须精确包含fixedquery;$:值到此结束,fixedquery之后不允许再有其他字符。
也就是说,只有值恰好等于 fixedquery 时才能通过校验。
如果你对正则表达式感到陌生也不必焦虑——它对很多人来说都是难点。好消息是你完全可以在暂时不需要它的前提下完成大量开发;当真正需要时,现在你已经知道在 FastAPI 中如何使用了。
值得一提的源码细节:早期 FastAPI 版本的参数叫 regex,而在 fastapi/params.py 中它已被标记为废弃(deprecated),并提示"FastAPI 0.100.0 起请改用 pattern",若仍传入 regex 会触发弃用警告(参见 fastapi/params.py#L48-L53 与警告逻辑)。这也是本教程使用 pattern 的原因。
非 None 的默认值
当然,默认值不必是 None。例如声明 q 的最小长度为 3,默认值为 "fixedquery":
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
注:只要参数带有默认值(无论默认值是否为
None),该参数就是可选的(optional,非必填)。反过来说,只有不写默认值,参数才是必填的。
必填参数(Required)
当不需要额外校验或元数据时,直接不写默认值即可让 q 成为必填参数:
q: str
替代之前的:
q: str | None = None
而当我们用 Query 声明时,例如:
q: Annotated[str | None, Query(min_length=3)] = None
如果想在 Query 参与声明的情况下仍让参数必填,同样只要不声明默认值:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Annotated[str, Query(min_length=3)]):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
此时不提供 q 会得到 422 校验错误。顺带说明,旧式的等价写法是:
q: str = Query(min_length=3)
必填但允许为 None
还有一种精细的需求:参数必须被客户端提供,但值允许是 None。声明方式是——把 None 放进合法类型集合,但不写默认值:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(min_length=3)]):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
这样客户端被强制发送该参数(例如显式 ?q=),即使其取值为 None 也能通过。
列表 / 多值查询参数
显式用 Query 声明查询参数时,还能让它接收一个值列表,也就是同名的多次出现参数。
例如让 q 在 URL 中可以出现多次:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Annotated[list[str] | None, Query()] = None):
query_items = {"q": q}
return query_items
然后请求这样的 URL:
http://localhost:8000/items/?q=foo&q=bar
多个 q 的值(foo 与 bar)就会以 Python list 的形式出现在函数参数 q 中。该 URL 的响应将是:
{
"q": [
"foo",
"bar"
]
}
提示:若要声明
list类型的查询参数,必须显式使用Query,否则 FastAPI 会把它当作 request body 解析(因为普通类型的列表参数默认对应请求体),而不是查询参数。
相应地,交互式 API 文档会自动更新以允许多值输入:
带默认值的列表参数
当客户端没有提供任何值时可返回默认列表:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@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 的默认值就是 ["foo", "bar"],响应为:
{
"q": [
"foo",
"bar"
]
}
只使用裸 list
也可以不使用 list[str],而是直接写 list:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Annotated[list, Query()] = []):
query_items = {"q": q}
return query_items
注:这种情况下 FastAPI 不会检查列表内部的元素类型。例如
list[int]会校验并文档化"列表内容必须是整数",而裸list则不做这种检查。
声明更多元数据:title 与 description
除了校验,你还可以为参数补充描述性元数据。这些信息会进入生成的 OpenAPI schema,进而被文档界面和外部工具消费。
注:不同工具对 OpenAPI 的支持程度可能不同,部分工具可能不会展示这里声明的全部附加信息;不过大多数情况下缺失的特性已在开发规划中。
添加 title:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(title="Query string", min_length=3)] = None,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
再添加 description:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
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,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
这些参数在 fastapi/params.py 的 Param.__init__ 签名中均有对应的关键字参数(title、description 等,见 fastapi/params.py#L29-L73),并会在生成 OpenAPI 时被逐项映射。
别名参数(alias):声明一个非法的 Python 标识符
想象你要让查询参数在 URL 中是 item-query:
http://127.0.0.1:8000/items/?item-query=foobaritems
但 item-query 并不是合法的 Python 变量名(含连字符),最接近的合法名字是 item_query。可你又确实需要它对外表现为 item-query——此时可以声明 alias,FastAPI 将使用该别名去 URL 中查找参数值:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
从源码看,alias 会被同时用作校验别名与序列化别名(当未单独指定 validation_alias/serialization_alias 时,它们默认继承 alias 的值,见 fastapi/params.py#L113-L116),保证"入参取值"与"文档展示"都使用该别名。
废弃参数(deprecated)
假设这个参数你不再喜欢了:由于客户端仍在用它,短期内不能直接删除,但你希望文档中明确把它标注为 deprecated(已废弃,建议不要使用)。
只需把 deprecated=True 传给 Query:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: 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,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
文档中该参数会以废弃样式显示:
上面这个例子同时集中演示了本教程的大部分能力:alias、title、description、min_length、max_length、pattern 与 deprecated 可以在一个 Query(...) 中组合使用。
从 OpenAPI 中排除参数:include_in_schema
如果希望某个查询参数不出现在生成的 OpenAPI schema 中(进而从自动文档系统中隐藏),把 Query 的 include_in_schema 设为 False 即可。参数在运行时依旧可用,只是不对外文档化:
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@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"}
从实现上看,include_in_schema 在 Param.__init__ 中默认值为 True(见 fastapi/params.py#L70),为 False 时 OpenAPI 生成环节会跳过该参数,适用于内部调试参数、密钥类等"不想暴露"的查询项。
自定义校验:结合 Pydantic 的 AfterValidator
有些场景需要的是上述参数无法覆盖的自定义校验逻辑。此时可以使用一个在常规校验(如"值必须是 str")之后执行的 custom validator function,通过 Pydantic 的 AfterValidator 放进 Annotated 实现。
提示:Pydantic 还提供
BeforeValidator等其他校验器。
下面这个例子中,自定义校验器会检查:item ID 必须以 isbn- 开头(面向 ISBN 图书编号),或以 imdb- 开头(面向 IMDB 电影 URL ID):
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需要 Pydantic 2 或更高版本。提示:如果校验需要与外部组件通信(如访问数据库或调用其他 API),应该改用 FastAPI Dependencies(依赖注入),后续教程会专门讲解。这类自定义校验器只适合处理那些仅凭请求中携带的数据本身即可完成判断的规则。
深入理解示例代码
str.startswith() 可接收元组
注意 value.startswith(...)(示例中即 id.startswith(...))可以接收一个元组,它会依次检查元组中的每一个值,任何一个前缀命中即返回 True。因此在 check_valid_id 中,只需一次调用即可判断 "isbn-" 与 "imdb-" 两种前缀。
随机推荐一个条目
当用户没有提供 id 时,示例会自动随机推荐一条数据:
data.items()返回一个可迭代对象,包含字典中每个条目的(key, value);- 用
list(data.items())把它转换为真正的列表; - 用
random.choice(...)随机取出一项,得到类似("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")的元组; - 再通过一次元组解包,把两个值分别赋给
id与name。
于是即使用户未传 ID,也总能收到一条随机的推荐结果——全部浓缩在一行 Python 中完成。
小结(Recap)
你可以在 FastAPI 中为参数声明附加校验与元数据,主要能力汇总如下。
通用校验与元数据:
| 参数 | 作用 |
|---|---|
alias |
声明参数的对外名称(允许非 Python 标识符) |
title |
参数的简短标题 |
description |
参数的详细描述 |
deprecated |
标记参数已废弃 |
include_in_schema |
是否将参数写入 OpenAPI schema |
针对字符串的特定校验:
| 参数 | 作用 |
|---|---|
min_length |
最小字符长度 |
max_length |
最大字符长度 |
pattern |
必须匹配的正则表达式 |
自定义校验:
- 通过
Annotated内嵌 Pydantic 的AfterValidator实现。
此外还应记住三个关键判定规则:
- 有无默认值决定是否必填:任何默认值(包括
None)都使参数可选; Annotated内不要再给Query传default,默认值应写在函数签名上;list类型的查询参数必须显式用Query,否则会被当作请求体解析。
本例演示了如何为 str 类型声明校验。在接下来的教程中,你还会学到如何为数字等其他类型声明类似的校验规则(如 gt、ge、lt、le 等,这些在 fastapi/params.py#L41-L44 的 Param 签名中同样可以看到)。
本文涉及的全部可直接运行的示例源码位于仓库的 docs_src/query_params_str_validations 目录(tutorial001~tutorial015 系列,_an_py310 后缀对应 Annotated 写法、_py310 后缀对应旧式默认值写法,均可对照阅读);Query 的底层实现与参数签名可查阅 fastapi/params.py;该教程的英文原文位于 docs/en/docs/tutorial/query-params-str-validations.md,本文所依据的多语言版本见 docs/hi/docs/tutorial/query-params-str-validations.md。
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 StartedRust0627
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

