FastAPI 如何按需拆分输入与输出的 OpenAPI Schema:`separate_input_output_schemas` 完全指南
自 Pydantic v2 推出后,FastAPI 生成的 OpenAPI 文档在精度与正确性上显著提升:同一个 Pydantic 模型在被用作请求体(输入)和响应体(输出)时,FastAPI 会依据是否存在默认值,为它在 OpenAPI 中生成两个不同的 JSON Schema(如 Item-Input 与 Item-Output)。本文以 docs/es/docs/how-to/separate-openapi-schemas.md 为主线,结合本仓库源码与测试,讲解这种拆分行为背后的机制、为何它能让 API 文档与自动生成的客户端/SDK 更精确,以及当你希望输入输出共用同一 Schema 时,如何通过 separate_input_output_schemas=False 关闭该特性。
为什么同一个模型会有两个 JSON Schema
一个带默认值的 Pydantic 模型
设想你有一个带默认值的 Pydantic 模型。参考本仓库源码示例 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
这里 description 带有默认值 None。这个看似简单的差异,会让它在"作为输入"与"作为输出"时拥有不同的 JSON Schema 语义。
作为输入:description 不必填
如果模型被用作请求体(输入),例如下面的 POST /items/:
app = FastAPI()
@app.post("/items/")
def create_item(item: Item):
return item
因为 description 有默认值 None,客户端可以不传该字段,所以输入侧的 JSON Schema 中它**不是必填(required)**的。
在 Swagger UI(/docs)中可以看到,请求体模型 Item-Input 里 name 带红色星号(必填),而 description 没有被标记为必填:
作为输出:description 一定存在
同一个模型被用作响应体(输出)时,情况就不同了。例如下面用 list[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"),
]
由于 description 有默认值,即便你没有为该字段显式返回任何值,序列化结果里它仍然会带上默认值。在 /docs 中实际执行该接口可以看到:第二条数据没有提供 description,但响应 JSON 中该字段依然存在,值为 null:
这意味着对使用该 API 的客户端而言,这个字段总会存在(只是值可能是 None,对应 JSON 里的 null)。所以客户端不需要判断该字段是否存在,可以直接访问。
为了在 OpenAPI 中准确描述这种"字段一定存在"的语义,输出侧的 JSON Schema 应当把这个字段标记为 required。因此,同一模型两种角色的差异就产生了:
- 作为输入:
description不要求必填; - 作为输出:
description标记为必填(并允许为null)。
在 /docs 中查看输出模型 Item-Output,可以看到 name 与 description 都带红色星号,均被标记为必填。若打开 OpenAPI 的 Schemas 列表,会发现模型被拆成了两个条目:
Item-Input:description非必填;Item-Output:description必填。
这种由 Pydantic v2 带来的能力,让 API 文档对同一模型的"入口契约"与"出口契约"表达得更精确;如果你的客户端或 SDK 由 OpenAPI 自动生成,那么生成的代码也会更精确——请求端不会多写不必填的字段,响应端则能安全地假设该字段始终可用,从而带来更好的开发者体验与类型一致性。
背后的实现机制(源码视角)
校验模式与序列化模式的分离
FastAPI 内部为每个模型字段维护了两种模式:validation(入参校验,即输入)与 serialization(响应序列化,即输出)。关键实现在 fastapi/_compat/v2.py 的 get_definitions()(第 285 行起):FastAPI 会把请求体字段按 validation 模式、响应字段按 serialization 模式分别展开为扁平模型集合,再交给 Pydantic 的 GenerateJsonSchema 生成定义,从而得到两份不同的 JSON Schema。
其中 get_schema_from_model_field()(fastapi/_compat/v2.py 第 254 行起)中有一段关键逻辑:
override_mode: Literal["validation"] | None = (
None
if (separate_input_output_schemas or _has_computed_fields(field))
else "validation"
)
从源码可以看出:当开启 separate_input_output_schemas 时,override_mode 为 None,即输入与输出分别保留各自的 validation / serialization 模式,从而生成两套 Schema;而当关闭该选项时,输出侧会被强制回落到 "validation" 模式,与输入共用同一套 Schema。另外,如果模型包含 Pydantic 的 computed_field,即便关闭了该选项也会强制使用序列化模式——因为计算字段只存在于输出侧。
参数如何在应用中流转
separate_input_output_schemas 是 FastAPI() 构造函数的参数,默认值为 True。在 fastapi/applications.py 第 780 行附近的 Docstring 中,给出了非常直观的说明:模型
class Item(BaseModel):
name: str
tags: list[str] = []
用作输入时 tags 不必填,客户端可以不提供;但用作输出时 tags 因为有默认值而始终存在(至少是空列表),客户端应始终能够读到它。FastAPI 会把该参数保存在 self.separate_input_output_schemas(第 890 行),并在构建 OpenAPI 时(第 1099 行)把它透传给 OpenAPI 生成器 fastapi/openapi/utils.py 中层层传递的 get_openapi、get_flat_models_from_routes 等函数,最终影响每个模型的 Schema 生成。
测试如何验证这一行为
仓库中的 tests/test_openapi_separate_input_output_schemas.py 对这一机制做了系统验证:它构造了同时作为请求体与响应体的 Item 模型(含嵌套 SubItem、带默认值的字段,以及开启 json_schema_serialization_defaults_required 的配置),并断言开启状态下生成的 OpenAPI 中出现 "$ref": "#/components/schemas/Item-Input" 与 "Item-Output" 两个条目;同时对比两种配置下 /items/、/items-list/ 等接口的实际请求与响应 JSON 保持一致。也就是说,拆分与否只影响 Schema 描述,不影响运行时的数据校验与序列化结果。对 computed_field 的处理则有 tests/test_computed_fields.py 覆盖。
不需要拆分时:separate_input_output_schemas=False
什么时候需要关闭拆分
某些场景下你希望输入与输出使用完全相同的 Schema。最常见的动机是:你已经有基于旧版 Schema 自动生成的客户端代码或 SDK,暂时不打算重新生成/升级它们(未来也许会的,但当下不想动)。此时如果文档突然把输出侧字段标成必填,旧客户端代码可能出现类型不匹配或静态检查告警。
如何关闭
在创建应用时传入 separate_input_output_schemas=False 即可,参考源码示例 docs_src/separate_openapi_schemas/tutorial002_py310.py:
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"),
]
改动只发生在应用实例化那一行;路径操作函数、模型定义都无需变化。应用仍照常启动,uvicorn main:app --reload 后打开 /docs 即可观察效果。
说明:
separate_input_output_schemas参数自 FastAPI 0.102.0 起提供。
关闭后的效果
关闭之后,OpenAPI 的 Schemas 列表中将不再出现 Item-Input / Item-Output 两个条目,而是只有一个统一的 Item Schema,请求体与响应体都引用它,description 恢复为非必填的输入侧语义:
决策建议
- 若你的 API 需要对外暴露最精确的契约(尤其是面向自动生成客户端/SDK、追求最佳开发者体验的场景),保持默认的
separate_input_output_schemas=True,让文档如实反映"输入可省略、输出必存在"的差异; - 若存在存量客户端或 SDK,且希望最小化兼容性冲击、暂时维持单一模型 Schema,再显式设置
separate_input_output_schemas=False; - 无论哪种选择,二者在运行时行为上完全一致(数据校验与响应序列化结果相同),差异仅体现在 OpenAPI /
/docs展示的 Schema 描述上,这由 tests/test_openapi_separate_input_output_schemas.py 的断言可以佐证。
小结
Pydantic v2 时代,FastAPI 得以依据字段是否有默认值,为输入与输出分别生成更精确的 JSON Schema:输出侧带默认值的字段会被标记为必填,因为它"总是存在"。这一行为默认开启,能让 /docs、自动生成的客户端与 SDK 对请求/响应契约的描述更准确;当你希望兼容存量客户端、让输入输出共用同一 Schema 时,则可以在创建 FastAPI() 时传入 separate_input_output_schemas=False 一键回到单一模型 Schema。相关可运行示例请参阅 docs_src/separate_openapi_schemas/tutorial001_py310.py 与 docs_src/separate_openapi_schemas/tutorial002_py310.py,原始英文文档见 docs/en/docs/how-to/separate-openapi-schemas.md。
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


