FastAPI 输入/输出 OpenAPI Schema 分离机制与 separate_input_output_schemas 参数详解
自 Pydantic v2 起,FastAPI 生成的 OpenAPI 文档变得更加精确:同一个 Pydantic 模型在请求体(输入)和响应体(输出)两种角色下,可能会生成两个不同的 JSON Schema——因为带默认值的字段在两种场景下的"必填"语义不同。本文基于官方文档 separate-openapi-schemas 教程,结合仓库中 fastapi/applications.py、fastapi/_compat/v2.py 的源码实现与测试用例,完整讲清这一机制的工作原理、对自动生成的客户端/SDK 的意义,以及如何用 separate_input_output_schemas=False 关闭 Schema 分离以保持客户端兼容性。
问题背景:一个模型,两种"必填"语义
考虑下面这个带默认值的 Pydantic 模型(来自 教程示例文件):
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
关键在于 description: str | None = None 这一行:它声明了默认值 None。这个默认值会让该字段在输入与输出场景下产生不同的必填语义:
- 作为输入(请求体)时:客户端可以不传
description,因为缺失时会自动使用默认值None——所以它是非必填字段; - 作为输出(响应体)时:序列化后的 JSON 中该字段一定存在(没设置时就是
null),客户端无需判断字段是否存在,可以直接假设它总在响应里——所以它应该被标记为必填字段。
OpenAPI 描述"字段总是存在"的方式就是把它列入 required 列表。于是同一个 Item 模型,在输入和输出两种用途下需要两个不同的 JSON Schema。
完整示例:同一模型同时用作输入和输出
下面是一个最小可运行的完整示例(docs_src/separate_openapi_schemas/tutorial001_py310.py 全文):
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
app = FastAPI()
@app.post("/items/")
def create_item(item: Item):
return item
@app.get("/items/")
def read_items() -> list[Item]:
return [
Item(
name="Portal Gun",
description="Device to travel through the multi-rick-verse",
),
Item(name="Plumbus"),
]
两个接口分别演示了模型的双重角色:
@app.post("/items/")中item: Item把Item用作输入(请求体校验);@app.get("/items/")的返回值注解-> list[Item]把Item用作输出(响应序列化与文档描述)。
输入视角:description 非必填
当 Item 用作请求体时,description 由于有默认值 None,不是必填字段。在 Swagger UI 中查看 Item-Input,description 字段没有红色星号标记,即未被标记为 required。
输出视角:description 必填(但值可以是 null)
当 Item 用作响应时,情况不同:即使你的代码没有给 description 赋任何值(如示例中的 Item(name="Plumbus")),序列化后的 JSON 响应中仍然会出现 "description": null——因为该字段有默认值,序列化时一定会输出。
这意味着使用你 API 的客户端不需要检查该字段是否存在,可以假设字段始终在响应中,只是某些情况下值为 None(JSON 中的 null)。在 OpenAPI 中描述这一点的方式就是把该字段标记为 required,因为它始终会出现。
因此,一个模型的 JSON Schema 会根据其用途(输入或输出)而不同:
- 作为输入时:
description非必填; - 作为输出时:
description必填(且可能为null)。
OpenAPI 中的两个 Schema:Item-Input 与 Item-Output
在 Swagger UI 的 Schemas 面板中(见文首截图)可以看到,同一个 Item 模型生成了两个 Schema:
Item-Input:description无红色星号,非必填;Item-Output:description带红色星号,必填。
这个行为正是 Pydantic v2 提供的能力:它区分"校验模式"(validation)与"序列化模式"(serialization),分别生成各自精确的 JSON Schema。FastAPI 直接利用了这一能力,使 API 文档更精确;如果你的客户端/SDK 是由 OpenAPI 文档自动生成的,生成的代码同样会更精确、更具一致性——比如输出模型中 description 会被生成为非可空缺省的字段,而不是可选字段。
源码纵深:分离机制是如何实现的
这一行为的开关贯穿 FastAPI 的 OpenAPI 生成调用链,可以沿以下源码路径追踪:
-
应用入口:fastapi/applications.py 中
FastAPI.__init__定义参数separate_input_output_schemas: Annotated[bool, ...](默认True),保存为实例属性;其内联文档还以tags: list[str] = []为例解释了输入/输出 Schema 差异。在生成 OpenAPI 时(约applications.py第 1099 行),该属性被传入get_openapi():separate_input_output_schemas=self.separate_input_output_schemas, -
OpenAPI 生成:fastapi/openapi/utils.py 中
get_openapi()、get_openapi_path_item()等函数层层透传separate_input_output_schemas,最终传给 Pydantic 兼容层的 Schema 生成函数。 -
核心判定逻辑:fastapi/_compat/v2.py 中的
get_definitions()与get_schema_from_model_field()是该机制的落点。关键逻辑是:override_mode: Literal["validation"] | None = ( None if (separate_input_output_schemas or _has_computed_fields(field)) else "validation" )含义是:
- 当
separate_input_output_schemas=True(默认)时,override_mode为None,字段按其原始模式(请求体为validation,响应为serialization)各自生成独立定义,从而产出Item-Input与Item-Output两份 Schema; - 当设为
False时,override_mode被强制为"validation",即输入与输出统一使用校验模式的 Schema(对应"非必填"语义); - 注意
_has_computed_fields(field)分支:只要模型含有@computed_field计算字段,就总是分离输入/输出 Schema(计算字段只在输出中存在,不可能在输入中出现,分离是唯一正确的描述方式),即使你显式设置了False。
- 当
测试用例佐证
仓库中的 tests/test_openapi_separate_input_output_schemas.py 用快照完整验证了两种模式下的 /openapi.json 输出:
- 默认模式下,
components.schemas中同时存在Item-Input(required: ["name"])与Item-Output(required: ["name", "description", "sub"]),请求体引用#/components/schemas/Item-Input,响应引用#/components/schemas/Item-Output; - 设置
separate_input_output_schemas=False后,只有单一的Item(required: ["name"]),输入与输出均引用#/components/schemas/Item; - 该测试还验证了含
computed_field的WithComputedField模型在False模式下依然保持WithComputedField-Input/WithComputedField-Output分离——与上文源码中_has_computed_fields的强制分支完全对应。
此外,嵌套模型同样会各自分离(如测试中的 SubItem-Input 与 SubItem-Output);而模型上的 model_config = {"json_schema_serialization_defaults_required": True} 配置(见该测试文件第 11、18 行)则控制 Pydantic 在序列化模式下是否把"带默认值"的字段一律标记为 required,是理解输出 Schema required 列表细节的配套机制。
关闭分离:separate_input_output_schemas=False
某些场景下你可能希望输入和输出共用同一个 Schema。文档中给出的主要用例是:你已经基于 OpenAPI 文档生成了一批客户端代码/SDK,暂时不想重新生成、更新所有客户端——未来会做,但现在不做。此时可以关闭该功能:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
app = FastAPI(separate_input_output_schemas=False)
@app.post("/items/")
def create_item(item: Item):
return item
@app.get("/items/")
def read_items() -> list[Item]:
return [
Item(
name="Portal Gun",
description="Device to travel through the multi-rick-verse",
),
Item(name="Plumbus"),
]
注意:
separate_input_output_schemas参数的支持是从 FastAPI 0.102.0 版本开始加入的。
关闭后,/openapi.json 中只剩一个 Schema Item,输入和输出都引用它,且 description 被标记为非必填(采用校验模式的语义)。
实践建议与小结
- 默认保持开启:
separate_input_output_schemas默认为True,生成的文档对 API 消费者(尤其是自动生成客户端)的描述最精确——输出模型中带默认值的字段保证存在,客户端可据此生成更严格的类型与反序列化逻辑。 - 仅在兼容性需要时关闭:当你已发布自动生成的 SDK 且不想引发客户端侧的模型变更(
Item变成Item-Input/Item-Output两个新类型)时,临时设为False,待客户端统一升级后再恢复默认行为。 - 注意例外:含
@computed_field的模型无论如何都会分离输入/输出 Schema,这是语义正确性的必然要求,不是配置失误。 - 验证方式:运行应用后直接查看
/openapi.json的components.schemas,或用 Swagger UI(/docs)的 Schemas 面板确认-Input/-Output后缀与红色星号是否符合预期;仓库中 test_openapi_schema_no_separate 的快照可视为"关闭模式"下 OpenAPI 输出的标准参照。
参考文件:
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 StartedRust0623
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

