首页
/ FastAPI 如何按需拆分输入与输出的 OpenAPI Schema:`separate_input_output_schemas` 完全指南

FastAPI 如何按需拆分输入与输出的 OpenAPI Schema:`separate_input_output_schemas` 完全指南

2026-09-07 17:56:34作者:邬祺芯Juliet

Pydantic v2 推出后,FastAPI 生成的 OpenAPI 文档在精度与正确性上显著提升:同一个 Pydantic 模型在被用作请求体(输入)和响应体(输出)时,FastAPI 会依据是否存在默认值,为它在 OpenAPI 中生成两个不同的 JSON Schema(如 Item-InputItem-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-Inputname 带红色星号(必填),而 description 没有被标记为必填:

Item-Input 输入模型中 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,可以看到 namedescription 带红色星号,均被标记为必填。若打开 OpenAPI 的 Schemas 列表,会发现模型被拆成了两个条目:

  • Item-Inputdescription 非必填;
  • Item-Outputdescription 必填。

Schemas 列表中 Item-Input 与 Item-Output 并存且必填语义不同

这种由 Pydantic v2 带来的能力,让 API 文档对同一模型的"入口契约"与"出口契约"表达得更精确;如果你的客户端或 SDK 由 OpenAPI 自动生成,那么生成的代码也会更精确——请求端不会多写不必填的字段,响应端则能安全地假设该字段始终可用,从而带来更好的开发者体验与类型一致性。

背后的实现机制(源码视角)

校验模式与序列化模式的分离

FastAPI 内部为每个模型字段维护了两种模式:validation(入参校验,即输入)与 serialization(响应序列化,即输出)。关键实现在 fastapi/_compat/v2.pyget_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_modeNone,即输入与输出分别保留各自的 validation / serialization 模式,从而生成两套 Schema;而当关闭该选项时,输出侧会被强制回落到 "validation" 模式,与输入共用同一套 Schema。另外,如果模型包含 Pydantic 的 computed_field,即便关闭了该选项也会强制使用序列化模式——因为计算字段只存在于输出侧。

参数如何在应用中流转

separate_input_output_schemasFastAPI() 构造函数的参数,默认值为 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_openapiget_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 恢复为非必填的输入侧语义:

关闭拆分后 Schemas 列表仅保留单一 Item Schema

决策建议

  • 若你的 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.pydocs_src/separate_openapi_schemas/tutorial002_py310.py,原始英文文档见 docs/en/docs/how-to/separate-openapi-schemas.md

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