首页
/ FastAPI 查询参数(Query)声明与字符串校验完整实战指南:Annotated、Validation 与 OpenAPI 元数据

FastAPI 查询参数(Query)声明与字符串校验完整实战指南:Annotated、Validation 与 OpenAPI 元数据

2026-09-07 16:01:23作者:曹令琨Iris

导读:本文将基于 FastAPI 官方教程中「Query Parameters and String Validations」一节的完整内容(对应中文环境下的教程文档 docs/hi/docs/tutorial/query-params-str-validations.md),系统讲解如何在路径操作函数中为查询参数声明额外校验规则元数据——从最基础的可选参数写法,到 Annotated + Query 的推荐用法、字符串的长度/正则校验、必填参数、多值列表,再到 aliasdeprecatedinclude_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 个字符。这是一类典型的字符串约束场景(例如控制日志关键词、搜索串的长度)。

导入 QueryAnnotated

要做到这一点,先完成两个导入:

  • 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 会自动做三件事:

  1. 校验(Validate)数据——确保长度不超过 50 个字符;
  2. 数据不合法时,向客户端返回清晰的错误信息(422 Validation Error);
  3. OpenAPI schema 的 path operation 中文档化该参数,使其显示在自动生成的交互式文档 UI 中。

从源码看,Queryfastapi/params.pyParam 的子类(fastapi/params.py#L221),其类属性 in_ = ParamTypes.query 将参数定位为 query 位置;而整个 Param 类则继承自 Pydantic 的 FieldInfofastapi/params.py#L26),因此 min_lengthmax_lengthpattern 这些约束最终都交由 Pydantic 字段校验系统落地,这也解释了为什么约束失败时返回的 422 错误结构与 Pydantic 校验错误一致。ParamTypes 枚举(queryheaderpathcookie)见 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 时,不能再给 Querydefault 参数,而应使用函数参数本身的默认值,否则语义会不一致。

以下写法是不允许的

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 的值(foobar)就会以 Python list 的形式出现在函数参数 q 中。该 URL 的响应将是:

{
  "q": [
    "foo",
    "bar"
  ]
}

提示:若要声明 list 类型的查询参数,必须显式使用 Query,否则 FastAPI 会把它当作 request body 解析(因为普通类型的列表参数默认对应请求体),而不是查询参数。

相应地,交互式 API 文档会自动更新以允许多值输入:

FastAPI Swagger UI 中查询参数 q 被声明为 string 数组并允许多值(foo、bar)的截图

带默认值的列表参数

当客户端没有提供任何值时可返回默认列表:

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.pyParam.__init__ 签名中均有对应的关键字参数(titledescription 等,见 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

文档中该参数会以废弃样式显示:

FastAPI Swagger UI 中 item-query 参数被标记为 deprecated 并展示其描述信息的截图

上面这个例子同时集中演示了本教程的大部分能力:aliastitledescriptionmin_lengthmax_lengthpatterndeprecated 可以在一个 Query(...) 中组合使用。

从 OpenAPI 中排除参数:include_in_schema

如果希望某个查询参数不出现在生成的 OpenAPI schema 中(进而从自动文档系统中隐藏),把 Queryinclude_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_schemaParam.__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") 的元组;
  • 再通过一次元组解包,把两个值分别赋给 idname

于是即使用户未传 ID,也总能收到一条随机的推荐结果——全部浓缩在一行 Python 中完成。

小结(Recap)

你可以在 FastAPI 中为参数声明附加校验与元数据,主要能力汇总如下。

通用校验与元数据:

参数 作用
alias 声明参数的对外名称(允许非 Python 标识符)
title 参数的简短标题
description 参数的详细描述
deprecated 标记参数已废弃
include_in_schema 是否将参数写入 OpenAPI schema

针对字符串的特定校验:

参数 作用
min_length 最小字符长度
max_length 最大字符长度
pattern 必须匹配的正则表达式

自定义校验:

  • 通过 Annotated 内嵌 Pydantic 的 AfterValidator 实现。

此外还应记住三个关键判定规则:

  1. 有无默认值决定是否必填:任何默认值(包括 None)都使参数可选;
  2. Annotated 内不要再给 Querydefault,默认值应写在函数签名上;
  3. list 类型的查询参数必须显式用 Query,否则会被当作请求体解析。

本例演示了如何为 str 类型声明校验。在接下来的教程中,你还会学到如何为数字等其他类型声明类似的校验规则(如 gtgeltle 等,这些在 fastapi/params.py#L41-L44Param 签名中同样可以看到)。

本文涉及的全部可直接运行的示例源码位于仓库的 docs_src/query_params_str_validations 目录(tutorial001tutorial015 系列,_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

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388