首页
/ FastAPI 查询参数与字符串校验完全指南:Annotated 与 Query 的深度实践

FastAPI 查询参数与字符串校验完全指南:Annotated 与 Query 的深度实践

2026-09-06 12:48:35作者:田桥桑Industrious

本文基于 FastAPI 官方教程《Query-Parameter und String-Validierungen》(查询参数与字符串校验章节)整理并扩展,系统讲解如何用 Querytyping.Annotated 为查询参数声明 min_lengthmax_lengthpattern 等字符串校验、alias/title/description/deprecated 等元数据,以及列表参数、必需参数和 Pydantic AfterValidator 自定义校验;并结合当前仓库源码 fastapi/params.py 与配套示例 docs_src/query_params_str_validations/ 说明这些参数的真实定义与行为边界,读完后你可以直接写出可复制运行的带完整校验与 OpenAPI 文档的查询接口。

FastAPI Swagger UI 中 deprecated=True 的查询参数展示

FastAPI Swagger UI 中支持多值的列表型查询参数展示

基础示例:可选的 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,能让你的编辑器提供更准确的类型补全和错误检测。

声明额外校验:QueryAnnotated

现在要保证:虽然 q 是可选的,但只要客户端提供了值,其长度就不能超过 50 个字符

导入 QueryAnnotated

为此需要导入两个东西:

  • 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 可以是 strNone,默认值为 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 会做到三件事:

  1. 校验数据,确保长度不超过 50 个字符;
  2. 当数据非法时向客户端返回清晰的错误信息
  3. 在 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 内部时,不允许再给 Querydefault 参数。默认值应写在函数参数本身上,否则语义会不一致。

例如这样写是不允许的:

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, use pattern instead.”(见 fastapi/params.py),因此新代码请一律使用 pattern

默认值(非 None

当然,默认值也可以不是 None。假设你希望 qmin_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 的形式收到多个值(foobar),响应如下:

{
  "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),只需给 Querydeprecated=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.pyQuery.__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()) 把它转成真正的 listrandom.choice() 从中随机取出一个元组,形如 ("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy");最后通过 id, item = ... 把元组的两个值分别赋给 iditem

因此,当用户没有提供 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_lengthmax_lengthpattern 三个字符串专用校验项与 titledescriptionaliasdeprecatedinclude_in_schema 等元数据项并列存在,且默认值均为 None(或未开启),印证了“只在显式声明时才生效”的行为;
  • pattern 之外还保留了已弃用的 regex 参数(FastAPI 0.100.0 起弃用,提示改用 pattern);
  • default 使用 Undefined 哨兵值,正是“Annotated 内禁止传 default、默认值由函数参数承载”这一约束在实现层的落点。

本章示例的配套测试与示例源码均集中在 docs_src/query_params_str_validations/ 目录(tutorial001_py310.pytutorial015_an_py310.py),可以直接对照本文各小节运行验证。

总结

你可以为参数声明额外的校验与元数据。分为三类:

通用校验与元数据(各类参数通用):

  • alias
  • title
  • description
  • deprecated

字符串专用校验:

  • min_length
  • max_length
  • pattern

自定义校验: 使用 Pydantic 的 AfterValidatorAnnotated 中挂载自定义校验函数。

本章演示了如何为 str 类型的查询参数声明校验;对于数字等其他类型的校验参数(如 gt/ge/lt/le/multiple_of 等,同样可在 fastapi/params.pyQuery 签名中见到),可继续查阅后续章节。

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