FastAPI Cookie 参数模型(Cookie Parameter Models)实战指南:用 Pydantic 模型批量声明、复用与严格校验 Cookie
在 FastAPI 中,当多个 Cookie 彼此相关时,可以用一个 Pydantic 模型一次性声明它们,从而把"散装"的 Cookie 参数整理成可复用、带校验与元数据的统一结构。本指南以本仓库教程 docs/es/docs/tutorial/cookie-param-models.md(对应英文原文 docs/en/docs/tutorial/cookie-param-models.md)为骨架,结合 docs_src/ 下的官方示例、fastapi/dependencies/utils.py 与 fastapi/openapi/utils.py 的源码实现,以及 tests/test_tutorial/test_cookie_param_models/ 中的真实测试,完整讲解 Cookie 参数模型的声明方式、校验规则、OpenAPI 文档表现与运行原理。读完你将能写出真正可复制的分组 Cookie 校验代码,并理解"额外 Cookie 被禁止"背后的实现机制。
为什么用 Pydantic 模型声明一组 Cookie
日常开发中,一个请求可能携带多条相关 Cookie,例如 session_id 与各类追踪标识。传统做法是在函数签名中逐个声明:
async def read_items(session_id: str, fatebook_tracker: str | None = None):
...
当 Cookie 数量增多,签名会迅速臃肿。把它们收进一个 Pydantic 模型则带来两大收益:
- 模型可在多处复用:同一个
Cookies模型可被不同路径操作、不同依赖反复引用,字段口径保持一致; - 一次声明校验与元数据:必填、可空、默认值、类型约束、字段说明等,全部集中在一个类定义里,由 FastAPI + Pydantic 统一执行。
需要留意两个前提:
- 该特性自 FastAPI
0.115.0版本起支持; - 同样的模型化声明技术同样适用于
Query、Cookie和Header(本仓库对应的进阶文档见 Query 参数模型 与 Header 参数模型,示例代码位于 docs_src/query_param_models/ 与 docs_src/header_param_models/)。
基础用法:把 Cookie 声明成 Pydantic 模型
官方示例 docs_src/cookie_param_models/tutorial001_an_py310.py 给出了完整可运行的最小实现:
from typing import Annotated
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies
代码拆解:
- 模型字段即 Cookie 名:模型
Cookies中的session_id、fatebook_tracker、googall_tracker字段,会逐一对应请求携带的同名 Cookie。 - 字段类型决定校验方式:
session_id: str是必填字符串;两个 tracker 字段声明为str | None = None,表示可选、缺省时为None。这里使用了 Python 3.10+ 的联合类型语法(示例文件名的py310后缀即指此意),字段默认值、Field级校验器(如min_length、pattern)等 Pydantic 能力均可照常使用。 Cookie()标记来源:用Annotated[Cookies, Cookie()]把类型标注与"这是从 Cookie 提取"的参数信息绑定在一起,FastAPI 因而知道该从request.cookies取数而不是当查询参数或请求体处理。
如果不习惯 Annotated,也可以使用等价的默认值写法(见 docs_src/cookie_param_models/tutorial001_py310.py):
@app.get("/items/")
async def read_items(cookies: Cookies = Cookie()):
return cookies
两种风格行为完全一致。FastAPI 会从请求携带的 Cookie 中,为模型中的每个字段逐一提取数据,最终交给你的就是填充完毕的 Pydantic 模型实例(此例中模型直接作为 JSON 响应返回)。
底层实现:字段如何被"摊平"再"聚合"
从源码看,模型化的 Cookie 参数并不会整体作为一个值去读取,而是先被摊平为一个个独立字段逐一解析,再重新组装成模型。核心逻辑位于 fastapi/dependencies/utils.py:
_get_flat_fields_from_params()(约 L157-L166):当某个参数恰好只有一个、且其注解是BaseModel子类时,通过get_cached_model_fields()取出模型的所有字段作为待处理字段,供 OpenAPI 生成与后续校验使用;request_params_to_args()(约 L780-L850):received_params在这里就是请求的 Cookie 映射。它先为每个模型字段用_get_multidict_value()从 Cookie 中取值(L809-L828),把未在模型中声明但实际收到的 Cookie 也并入待校验字典(L830-L839);当发现是"单个未嵌入的模型字段"时(single_not_embedded_field),会把整份 Cookie 数据一次性交给模型做整体校验(L841-L850),这正是 Pydantic 的extra配置能在后文"禁止额外 Cookie"中生效的关键。
运行效果可用如下命令验证(uvicorn 启动后):
# 缺少必填 cookie session_id,返回 422 校验错误
curl -i http://127.0.0.1:8000/items/
# 带上全部 cookie,正常返回模型 JSON
curl -i http://127.0.0.1:8000/items/ \
-H "Cookie: session_id=123; fatebook_tracker=456; googall_tracker=789"
上述行为的权威佐证来自仓库测试 tests/test_tutorial/test_cookie_param_models/test_tutorial001.py:
test_cookie_param_model:三个 Cookie 全部设置时返回200,响应 JSON 与模型字段一一对应;test_cookie_param_model_defaults:只设置session_id时,两个可选字段被填充为None;test_cookie_param_model_invalid:完全不带 Cookie 时返回422,错误定位为loc: ["cookie", "session_id"],类型missing,信息Field required;test_cookie_param_model_extra:默认模型配置下多发送一个未声明的extraCookie 会被静默忽略(Pydantic v2 默认extra="ignore"),仍返回200;test_openapi_schema:断言生成的/openapi.json中,session_id等字段各自成为独立参数,且in: "cookie",required标志与字段是否必填一致。
注意:示例文件名中的 an(Annotated 风格)与纯 py310 风格两个变体都会被同一组测试参数化覆盖(见测试文件顶部的 pytest.fixture 与 needs_py310 标记)。
在 /docs 交互式文档中查看 Cookie
声明完成后,可以打开 /docs 页面确认参数已正确暴露给 API 使用者。下图即官方示例截图,展示本特性的文档界面表现:
之所以每个模型字段都能以独立参数出现在文档与 openapi.json 中,是因为 OpenAPI 构建阶段同样使用了"摊平"逻辑:fastapi/openapi/utils.py 中 get_flat_params(api_route.dependant)(约 L576)会展开模型字段,生成 in: "cookie" 的参数条目。也就是说,Cookie 模型既参与运行时的请求校验,也参与契约(OpenAPI Schema)的自动生成。
一个重要的浏览器限制
在使用 /docs 交互界面测试时请留意:浏览器对 Cookie 有特殊、幕后的管理方式,JavaScript 无法随意读写它们。因此:
- 你可以在
/docs的 UI 上看到所有 path operations 的 Cookie 参数文档; - 但即便在参数框里填好数据并点击 "Execute",由于文档 UI 是通过 JavaScript 发起请求的,这些 Cookie 并不会真正随请求发送,你会看到类似"未填写任何值"的校验错误。
这不是代码缺陷,而是浏览器安全模型的固有行为。要真实联调 Cookie 场景,应使用带 Cookie 存储的 HTTP 客户端(如 curl 的 -H "Cookie: ..."、浏览器开发者工具,或仓库测试所用的 TestClient 的 cookies.set() 接口)。
进阶:禁止接收额外 Cookie
某些特殊场景下(通常并不常见),你可能希望 API 只接受白名单内的 Cookie,拒绝任何额外项。官方示例 docs_src/cookie_param_models/tutorial002_an_py310.py 展示了通过 Pydantic 模型配置实现这一点:
from typing import Annotated
from fastapi import Cookie, FastAPI
from pydantic import BaseModel
app = FastAPI()
class Cookies(BaseModel):
model_config = {"extra": "forbid"}
session_id: str
fatebook_tracker: str | None = None
googall_tracker: str | None = None
@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
return cookies
与基础版相比,只多了一行 model_config = {"extra": "forbid"}(纯默认值写法见 docs_src/cookie_param_models/tutorial002_py310.py)。这会把 Pydantic v2 对额外输入的处理策略从默认的 "ignore" 切换为 "forbid",于是任何未在模型中声明的 Cookie 都会触发校验错误响应。
例如,若客户端发送了一个值为 good-list-please 的 santa_tracker Cookie,响应将明确告知该 Cookie 不被允许:
{
"detail": [
{
"type": "extra_forbidden",
"loc": ["cookie", "santa_tracker"],
"msg": "Extra inputs are not permitted",
"input": "good-list-please"
}
]
}
从错误结构可以反推实现路径:loc 的第一段是 "cookie"(即 params.Param 中 in_ 的分类值,见 fastapi/params.py 中 Cookie 类 L387-L388 的定义 in_ = ParamTypes.cookie),第二段是违规的 Cookie 名。这正是前文所述 request_params_to_args() 把未声明 Cookie 并入待校验字典、再交给 _validate_value_with_model_field() 做整体模型校验的结果——extra_forbidden 错误来自 Pydantic 对多出字段的拒绝。
该行为的权威验证位于 tests/test_tutorial/test_cookie_param_models/test_tutorial002.py:
test_cookie_param_model/test_cookie_param_model_defaults:白名单内的 Cookie 行为与基础版一致;test_cookie_param_model_extra:在设置了session_id后额外发送名为extra、值为track-me-here-too的 Cookie,此时返回422,错误体为extra_forbidden,loc为["cookie", "extra"],msg为Extra inputs are not permitted,input原样带回非法值;test_openapi_schema:确认开启动态白名单后,OpenAPI 参数列表与基础版一致(模型字段仍是独立 Cookie 参数)。
运行原理与实现细节速览
为了让"模型化 Cookie"与"普通 Cookie 参数"在 FastAPI 内部达成统一,框架做了如下分层处理:
| 层面 | 关键位置 | 作用 |
|---|---|---|
| 参数类型定义 | fastapi/params.py Cookie(Param)(L387) |
定义 Cookie 参数描述类,in_ = ParamTypes.cookie,继承自 Pydantic 的 FieldInfo |
| 依赖分析与字段摊平 | fastapi/dependencies/utils.py _get_flat_fields_from_params(L157) |
当单个参数注解为 BaseModel 时,将其展开为多个独立字段,参与请求校验与文档生成 |
| 运行时取数 | 同文件 request_params_to_args(L780) |
从 Cookie 映射逐字段取值、合并未声明项,最后整体校验并重建模型 |
| OpenAPI 生成 | fastapi/openapi/utils.py get_flat_params(约 L576) |
展开模型字段,把每个字段输出为 in: "cookie" 的参数项 |
值得一提的实现细节:request_params_to_args() 接收的参数源类型是 Mapping | QueryParams | Headers,对 Header 场景还有 convert_underscores 的特殊处理(L799-L803、L811-L828)。Cookie 模型之所以与 Query、Header 模型共享同一套技术,正是因为在源码层面它们都走这一统一的"摊平—取数—重建"管线,只是 field_info.in_.value("cookie" / "query" / "header")不同,相应地读取来源也不同。字段别名方面,取数与错误定位都经由 get_validation_alias() 获得实际使用的键名,因此模型上通过 Pydantic 配置的验证别名同样会被尊重。
如何运行示例与测试
所有示例源码与配套测试都已在仓库内,可以直接验证:
# 运行 Cookie 参数模型教程的全部测试(含 Annotated 与默认值两种风格、extra 行为与 OpenAPI schema)
pytest tests/test_tutorial/test_cookie_param_models/ -q
# 也可启动本地服务手动联调(需先按 pyproject.toml 安装依赖)
python -m uvicorn docs_src.cookie_param_models.tutorial001_an_py310:app --reload
测试中通过 TestClient 的上下文管理器与 cookies.set() 写入模拟 Cookie(见 test_tutorial001.py 与 test_tutorial002.py 的 client fixture),分别覆盖了"全部命中""部分缺省""必填缺失 422""额外 Cookie 忽略/拒绝 422""OpenAPI 参数结构"五类场景,可作为你理解或扩展自定义行为时的参照。此外,文档截图由仓库内的 Playwright 脚本 scripts/playwright/cookie_param_models/image01.py 自动生成,印证了 /docs 页面会以独立参数形式渲染 Cookie 模型字段。
小结
- 当多个 Cookie 相互关联时,可以在 Pydantic 模型中统一声明,实现跨路径复用与集中式校验/元数据管理;
- 声明方式是在路径操作参数上使用
Cookie(),并让类型注解指向该模型;Annotated与默认值两种写法等价; - 模型字段名默认即 Cookie 名,必填与否由字段类型与默认值决定,缺少必填 Cookie 会返回
422; - 在
/docs中每个字段都会以in: "cookie"的独立参数呈现,但由于浏览器限制,文档 UI 的 "Execute" 无法真正发送 Cookie,联调请使用curl、开发者工具或TestClient; - 通过
model_config = {"extra": "forbid"}可拒绝任何未声明的额外 Cookie,违规响应为422,错误类型extra_forbidden、loc形如["cookie", "额外Cookie名"]。
以上内容均可在当前仓库中对照验证:教程原文见 docs/es/docs/tutorial/cookie-param-models.md 与 docs/en/docs/tutorial/cookie-param-models.md,示例代码见 docs_src/cookie_param_models/,实现源码见 fastapi/dependencies/utils.py 与 fastapi/openapi/utils.py,行为断言见 tests/test_tutorial/test_cookie_param_models/。
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
