FastAPI 查询参数与字符串校验完全指南:Annotated 与 Query 的深度实践
本文基于 FastAPI 官方教程《Query-Parameter und String-Validierungen》(查询参数与字符串校验章节)整理并扩展,系统讲解如何用 Query 与 typing.Annotated 为查询参数声明 min_length、max_length、pattern 等字符串校验、alias/title/description/deprecated 等元数据,以及列表参数、必需参数和 Pydantic AfterValidator 自定义校验;并结合当前仓库源码 fastapi/params.py 与配套示例 docs_src/query_params_str_validations/ 说明这些参数的真实定义与行为边界,读完后你可以直接写出可复制运行的带完整校验与 OpenAPI 文档的查询接口。
基础示例:可选的 q 查询参数
先看本章贯穿始终的基础应用(完整源码见 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,意味着它既可以是 str 也可以是 None,并且默认值就是 None,因此 FastAPI 会自动判定该参数非必需(optional)。
说明:FastAPI 正是依据默认值
= None识别出q不需要客户端必传。使用str | None而不是裸的str,能让你的编辑器提供更准确的类型补全和错误检测。
声明额外校验:Query 与 Annotated
现在要保证:虽然 q 是可选的,但只要客户端提供了值,其长度就不能超过 50 个字符。
导入 Query 与 Annotated
为此需要导入两个东西:
- 从
fastapi导入Query; - 从
typing导入Annotated。
from typing import Annotated
from fastapi import FastAPI, Query
版本前提:FastAPI 自 0.95.0 起支持并推荐
Annotated风格。如果你的 FastAPI 版本低于 0.95.0,使用Annotated会直接报错。请先升级(至少到 0.95.1)再使用该写法,官方升级说明见 docs/de/docs/deployment/versions.md。
用 Annotated 包装 q 的类型注解
在 FastAPI 的 Python 类型入门章节 中提到过:Annotated 可以用于给参数附加元数据。现在就该用它了。
原本的注解是:
q: str | None = None
把它包进 Annotated 后变成:
q: Annotated[str | None] = None
这两个写法在类型语义上完全等价:q 可以是 str 或 None,默认值为 None。真正有意思的用法是把 Query 放进 Annotated 里——这是元数据的位置。
把 Query(max_length=50) 放进 Annotated
在 Annotated 中追加 Query,并把参数 max_length 设为 50(完整源码见 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
注意默认值仍然是 None,参数依旧可选;而 Annotated 中的 Query(max_length=50) 则是告诉 FastAPI:对这个值执行额外校验——最长 50 个字符。
提示:这里用
Query()是因为它是一个查询参数。后面你会看到Path()、Body()、Header()、Cookie()也都接受与Query()相同风格的校验参数(它们在 fastapi/params.py 中同源于Param基类)。
此时 FastAPI 会做到三件事:
- 校验数据,确保长度不超过 50 个字符;
- 当数据非法时向客户端返回清晰的错误信息;
- 在 OpenAPI Schema 的路径操作中文档化该参数,使其出现在自动文档界面(Swagger UI)里。
替代写法(旧风格):把 Query 作为默认值
在 0.95.0 之前的旧版本中,必须把 Query 当作参数的默认值来使用,而不是放在 Annotated 里。你在大量存量代码中仍会看到这种写法(完整源码见 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 作为默认值还是放进 Annotated
两者有一个关键差异:当 Query 位于 Annotated 内部时,不允许再给 Query 传 default 参数。默认值应写在函数参数本身上,否则语义会不一致。
例如这样写是不允许的:
q: Annotated[str, Query(default="rick")] = "morty"
因为 FastAPI 无法判断默认值到底应该是 "rick" 还是 "morty"。正确的写法是:
q: Annotated[str, Query()] = "rick"
而在旧代码库中你会见到等价写法:
q: str = Query(default="rick")
也就是说,在不使用 Annotated 的情况下,函数默认值位置被 Query() 占据,需要用 Query(default=None) 来承担原来默认值 None 的职责:
q: str | None = Query(default=None)
它与 q: str | None = None 对 FastAPI 而言效果相同(参数可选、默认 None),但 Query 版本显式声明了这是一个查询参数。之后再传入校验参数即可:
q: str | None = Query(default=None, max_length=50)
同样会执行校验、返回清晰错误,并把参数写入 OpenAPI 文档。
Annotated 的优势
官方明确推荐使用 Annotated 代替“函数默认值”风格,原因如下:
- 函数参数本身的默认值就是真实的默认值。这更符合 Python 的直觉习惯;
- 该函数可以在 FastAPI 之外的地方被调用,且行为符合预期。如果存在必需参数(无默认值),编辑器会报类型错误,Python 运行时也会在缺少参数时抛出
TypeError; - 若使用旧的默认值风格,你在别处直接调用该函数时必须记住手工传入参数,否则拿到的将是
QueryInfo(参数信息对象)而不是真正的str。编辑器帮不了你,Python 也不会抱怨,直到函数内部逻辑真正出错; Annotated可以承载多个元数据注解,因此同一个函数还能被其他工具(如 Typer 等 CLI 框架)复用。
添加更多字符串校验:min_length
在 max_length 之外还可以叠加 min_length(完整源码见 tutorial003_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(min_length=3, max_length=50)] = None,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
添加正则表达式:pattern
可以为参数声明一个 pattern(正则表达式),参数值必须与之匹配(完整源码见 tutorial004_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(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之后不能有任何字符。
如果你对手写正则感到吃力也不必担心——它是公认的难度点,很多场景用不上;现在你只需知道,当确实需要时 FastAPI 的查询参数随时可以挂上 pattern。
实现细节:
regex参数在 FastAPI 0.100.0 起已被标记为弃用,源码中明确注释“Deprecated in FastAPI 0.100.0 and Pydantic v2, usepatterninstead.”(见 fastapi/params.py),因此新代码请一律使用pattern。
默认值(非 None)
当然,默认值也可以不是 None。假设你希望 q 有 min_length=3 的校验,且默认值为 "fixedquery"(完整源码见 tutorial005_an_py310.py):
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)都会使参数变为可选(非必需)。
必需参数(Required)
如果不需要额外校验或元数据,直接不写默认值就能把查询参数变成必需:
q: str
而不是:
q: str | None = None
当使用了 Query 时,同理——不声明默认值即为必需,例如(完整源码见 tutorial006_an_py310.py):
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
必需,但可以是 None
你可以声明一个参数允许取 None 值,但仍然是必需的——客户端必须显式发送该值,哪怕发送的就是 None。做法是:声明类型包含 None,但不给默认值(完整源码见 tutorial006c_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(min_length=3)]):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
这里 str | None 表示 None 是合法类型,但因为没有默认值,请求中必须包含 q。
查询参数列表 / 多值参数
当显式使用 Query 定义查询参数时,可以把它声明为接收值列表的参数,即允许多个值。例如让 q 在 URL 中出现多次(完整源码见 tutorial011_an_py310.py):
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 在路径操作函数的函数参数中会以 Python list 的形式收到多个值(foo 和 bar),响应如下:
{
"q": [
"foo",
"bar"
]
}
提示:要声明
list类型的查询参数,必须显式使用Query,否则 FastAPI 会把它当成请求体(Request body)来解析。
交互式 API 文档也会相应更新,把该参数标记为允许多个值(对应本文开头的第二张截图)。
带默认值的列表参数
可以定义一个默认列表,当客户端没有提供任何值时使用(完整源码见 tutorial012_an_py310.py):
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(完整源码见 tutorial013_an_py310.py):
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不会做这类内容校验。
声明更多元数据
你还可以为参数附加更多信息,这些信息会进入生成的 OpenAPI,并被文档界面和外部工具使用(不同工具对 OpenAPI 各字段的渲染支持程度可能不同,个别字段可能在某些界面中暂不显示)。
添加 title(完整源码见 tutorial007_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(title="Query string", min_length=3)] = None,
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
以及 description(完整源码见 tutorial008_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(
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
别名参数(alias)
假设你希望 URL 中的参数名叫 item-query,即:
http://127.0.0.1:8000/items/?item-query=foobaritems
但 item-query 不是合法的 Python 变量名,最接近的写法是 item_query。如果你需要 URL 里精确是 item-query,可以声明 alias,FastAPI 会用该别名来查找参数值(完整源码见 tutorial009_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(alias="item-query")] = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
把参数标记为弃用(deprecated)
如果某个参数你不再希望客户端使用,但因兼容存量客户端必须暂时保留,同时又想让文档明确提示它已弃用(deprecated),只需给 Query 传 deprecated=True(完整源码见 tutorial010_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(
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
自动文档界面会将其渲染为弃用状态,即本文开头第一张截图中该参数的展示样式。
把参数从 OpenAPI 中排除(include_in_schema)
若想让某个查询参数不出现在生成的 OpenAPI Schema 中(从而也不出现在自动文档界面里),把 Query 的参数 include_in_schema 设为 False(完整源码见 tutorial014_an_py310.py):
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"}
该参数仍然正常参与请求解析与校验,只是对文档与外部工具“隐身”。在 fastapi/params.py 的 Query.__init__ 签名中可以看到,include_in_schema: bool = True 是默认开启文档化的。
自定义校验:AfterValidator
有些业务规则无法用 Query 内置参数表达,此时可以使用自定义校验函数。该函数在常规校验完成之后(例如值已被确认是 str)执行。在 Annotated 内使用 Pydantic 的 AfterValidator 即可实现(完整源码见 tutorial015_an_py310.py):
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 v2 及以上版本。Pydantic 同时提供BeforeValidator等多种校验器可按需选用。
提示:如果校验逻辑需要与外部组件通信(例如查数据库或调用另一个 API),请改用 FastAPI 依赖(Dependencies),而不是自定义校验器。这类校验器适合用请求中已有的数据即可判定的规则。
读懂这段代码
关键点只有一个:在 Annotated 里把一个函数传给 AfterValidator。下面是对示例细节的拆解。
用 value.startswith() 判断字符串前缀
Python 的 str.startswith() 可以接受一个元组,只要值以元组中任意一项开头即返回 True:
if not id.startswith(("isbn-", "imdb-")):
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
一个随机条目
data.items() 得到一个可迭代对象,包含每个字典键值对组成的元组;list(data.items()) 把它转成真正的 list;random.choice() 从中随机取出一个元组,形如 ("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy");最后通过 id, item = ... 把元组的两个值分别赋给 id 与 item。
因此,当用户没有提供 id 时,接口仍会随机返回一条建议记录——所有这些逻辑压缩在一行里完成:
id, item = random.choice(list(data.items()))
源码佐证:Query 参数在 FastAPI 中的真实定义
上面的所有能力都来自 fastapi/params.py 中的 Query 类(继承自 Param,并设置 in_ = ParamTypes.query)。从源码签名可以确认字符串校验相关的可用参数:
class Query(Param): # type: ignore[misc]
in_ = ParamTypes.query
def __init__(
self,
default: Any = Undefined,
*,
default_factory: Callable[[], Any] | None = _Unset,
annotation: Any | None = None,
alias: str | None = None,
...
title: str | None = None,
description: str | None = None,
...
min_length: int | None = None,
max_length: int | None = None,
pattern: str | None = None,
...
deprecated: deprecated | str | bool | None = None,
include_in_schema: bool = True,
...
):
从源码结构看:
min_length、max_length、pattern三个字符串专用校验项与title、description、alias、deprecated、include_in_schema等元数据项并列存在,且默认值均为None(或未开启),印证了“只在显式声明时才生效”的行为;pattern之外还保留了已弃用的regex参数(FastAPI 0.100.0 起弃用,提示改用pattern);default使用Undefined哨兵值,正是“Annotated内禁止传default、默认值由函数参数承载”这一约束在实现层的落点。
本章示例的配套测试与示例源码均集中在 docs_src/query_params_str_validations/ 目录(tutorial001_py310.py 至 tutorial015_an_py310.py),可以直接对照本文各小节运行验证。
总结
你可以为参数声明额外的校验与元数据。分为三类:
通用校验与元数据(各类参数通用):
aliastitledescriptiondeprecated
字符串专用校验:
min_lengthmax_lengthpattern
自定义校验: 使用 Pydantic 的 AfterValidator 在 Annotated 中挂载自定义校验函数。
本章演示了如何为 str 类型的查询参数声明校验;对于数字等其他类型的校验参数(如 gt/ge/lt/le/multiple_of 等,同样可在 fastapi/params.py 的 Query 签名中见到),可继续查阅后续章节。
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 StartedRust0625
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

