首页
/ FastAPI 输入/输出 OpenAPI Schema 分离机制与 separate_input_output_schemas 参数详解

FastAPI 输入/输出 OpenAPI Schema 分离机制与 separate_input_output_schemas 参数详解

2026-09-04 18:42:39作者:苗圣禹Peter

自 Pydantic v2 起,FastAPI 生成的 OpenAPI 文档变得更加精确:同一个 Pydantic 模型在请求体(输入)和响应体(输出)两种角色下,可能会生成两个不同的 JSON Schema——因为带默认值的字段在两种场景下的"必填"语义不同。本文基于官方文档 separate-openapi-schemas 教程,结合仓库中 fastapi/applications.pyfastapi/_compat/v2.py 的源码实现与测试用例,完整讲清这一机制的工作原理、对自动生成的客户端/SDK 的意义,以及如何用 separate_input_output_schemas=False 关闭 Schema 分离以保持客户端兼容性。

Swagger UI 中展示 Item-Input 与 Item-Output 两个 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: ItemItem 用作输入(请求体校验);
  • @app.get("/items/") 的返回值注解 -> list[Item]Item 用作输出(响应序列化与文档描述)。

输入视角:description 非必填

Item 用作请求体时,description 由于有默认值 None不是必填字段。在 Swagger UI 中查看 Item-Inputdescription 字段没有红色星号标记,即未被标记为 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-Inputdescription 无红色星号,非必填;
  • Item-Outputdescription 带红色星号,必填。

这个行为正是 Pydantic v2 提供的能力:它区分"校验模式"(validation)与"序列化模式"(serialization),分别生成各自精确的 JSON Schema。FastAPI 直接利用了这一能力,使 API 文档更精确;如果你的客户端/SDK 是由 OpenAPI 文档自动生成的,生成的代码同样会更精确、更具一致性——比如输出模型中 description 会被生成为非可空缺省的字段,而不是可选字段。

源码纵深:分离机制是如何实现的

这一行为的开关贯穿 FastAPI 的 OpenAPI 生成调用链,可以沿以下源码路径追踪:

  1. 应用入口fastapi/applications.pyFastAPI.__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,
    
  2. OpenAPI 生成fastapi/openapi/utils.pyget_openapi()get_openapi_path_item() 等函数层层透传 separate_input_output_schemas,最终传给 Pydantic 兼容层的 Schema 生成函数。

  3. 核心判定逻辑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_modeNone,字段按其原始模式(请求体为 validation,响应为 serialization)各自生成独立定义,从而产出 Item-InputItem-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-Inputrequired: ["name"])与 Item-Outputrequired: ["name", "description", "sub"]),请求体引用 #/components/schemas/Item-Input,响应引用 #/components/schemas/Item-Output
  • 设置 separate_input_output_schemas=False 后,只有单一的 Itemrequired: ["name"]),输入与输出均引用 #/components/schemas/Item
  • 该测试还验证了含 computed_fieldWithComputedField 模型在 False 模式下依然保持 WithComputedField-Input / WithComputedField-Output 分离——与上文源码中 _has_computed_fields 的强制分支完全对应。

此外,嵌套模型同样会各自分离(如测试中的 SubItem-InputSubItem-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 被标记为非必填(采用校验模式的语义)。

关闭分离后 Swagger UI 中仅有一个 Item Schema

实践建议与小结

  • 默认保持开启separate_input_output_schemas 默认为 True,生成的文档对 API 消费者(尤其是自动生成客户端)的描述最精确——输出模型中带默认值的字段保证存在,客户端可据此生成更严格的类型与反序列化逻辑。
  • 仅在兼容性需要时关闭:当你已发布自动生成的 SDK 且不想引发客户端侧的模型变更(Item 变成 Item-Input/Item-Output 两个新类型)时,临时设为 False,待客户端统一升级后再恢复默认行为。
  • 注意例外:含 @computed_field 的模型无论如何都会分离输入/输出 Schema,这是语义正确性的必然要求,不是配置失误。
  • 验证方式:运行应用后直接查看 /openapi.jsoncomponents.schemas,或用 Swagger UI(/docs)的 Schemas 面板确认 -Input/-Output 后缀与红色星号是否符合预期;仓库中 test_openapi_schema_no_separate 的快照可视为"关闭模式"下 OpenAPI 输出的标准参照。

参考文件

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