
FastAPI 详解 OpenAPI 输入输出 Schema 分离机制与 separate_input_output_schemas 参数【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi本篇指南围绕 FastAPI 官方文档中为输入和输出分离 OpenAPI Schema这一主题展开自 Pydantic v2 起同一个 Pydantic 模型在 OpenAPI 文档中可能同时生成Item-Input和Item-Output两份 JSON Schema以精确区分请求体必填字段与响应体必现字段。读完本文后你将理解两种 Schema 各自required差异背后的语义、如何通过 FastAPI 参数separate_input_output_schemasFalse关闭该行为以兼容旧版自动生成客户端并能从 FastAPI 源码中追到这一机制的实现位置与边界条件。背景Pydantic v2 带来的更精确的 OpenAPI自Pydantic v2发布以来FastAPI 生成的 OpenAPI 文档变得比之前更精确、也更正确。在某些情况下即使是同一个 Pydantic 模型OpenAPI 中也会出现两份 JSON Schema——一份用于输入请求体一份用于输出响应体。区分的关键依据是该模型的字段是否存在默认值。这个机制的核心收益在于API 文档以及基于文档自动生成的客户端和 SDK可以更准确地描述调用方必须提供什么和调用方一定能收到什么从而提升开发者体验与前后端契约的一致性。示例模型一个带默认值的字段来看一个典型的 Pydantic 模型其description字段带有默认值完整源码见 tutorial001_py310.pyclass Item(BaseModel): name: str description: str | None None注意两个要点name: str没有默认值因此它是必填字段description: str | None None有默认值None因此它是可选字段——但可选这个说法在输入和输出场景下含义完全不同这正是本文的主题。用作输入带默认值的字段不再是必填当这个模型作为输入使用时例如app FastAPI() app.post(/items/) def create_item(item: Item): return item……那么description字段就不是必填的因为它有默认值None。你可以在 Swagger UI 文档中确认这一点description字段旁边没有红色星号即没有被标记为 required。用 OpenAPI 术语表述请求体引用的是Item-Inputschema其required数组只包含[name]。用作输出带默认值的字段总是存在但如果把同一个模型用作输出例如app.get(/items/) def read_items() - list[Item]: return [ Item( namePortal Gun, descriptionDevice to travel through the multi-rick-verse, ), Item(namePlumbus), ]……那么由于description有默认值即使端点代码在返回时没有为它赋值如示例中的Item(namePlumbus)序列化结果中它依然会带有那个默认值。这一点可以从实际响应中得到验证虽然代码没有为其中一条数据的description赋值但 JSON 响应中仍然包含默认值null[ { name: Portal Gun, description: Device to travel through the multi-rick-verse }, { name: Plumbus, description: null } ]这意味着该字段永远会有一个值只是这个值有时可能是None在 JSON 中即null。由此带来的客户端语义是使用你 API 的客户端不需要检查该字段是否存在它可以假定该字段一定存在只是在某些情况下值为默认值None。要在 OpenAPI 中准确描述这种永远存在的语义唯一正确的做法就是把该字段标记为required。结论同一个模型两种不同的 JSON Schema正因为输入与输出的必填语义不同一个模型的 JSON Schema 会因用途而异作为输入时description不是 required作为输出时description是 required且可能取None即 JSON 的null。你可以在文档的 Schemas 面板中直接看到这两份 schema一份叫Item-Inputdescription没有红色星号另一份叫Item-Outputdescription带红色星号。这正是Pydantic v2带来的能力API 文档更精确如果还有基于 OpenAPI 自动生成的客户端和 SDK它们也会更精确开发者体验和一致性都更好。测试用例 test_openapi_separate_input_output_schemas.py 对生成的/openapi.json做了完整快照断言可以直接印证上述结构。默认行为分离模式下Item-Input: { type: object, title: Item, required: [name], properties: { name: {type: string, title: Name}, description: {anyOf: [{type: string}, {type: null}], title: Description}, sub: {anyOf: [{$ref: #/components/schemas/SubItem-Input}, {type: null}]} } }, Item-Output: { type: object, title: Item, required: [name, description, sub], properties: { name: {type: string, title: Name}, description: {anyOf: [{type: string}, {type: null}], title: Description}, sub: {anyOf: [{$ref: #/components/schemas/SubItem-Output}, {type: null}]} } }可以看到同一模型的Item-Input的required只有[name]而Item-Output的required扩展为[name, description, sub]——所有带默认值的字段在输出 schema 中都变成了必填。此外该测试还覆盖了一个重要边界嵌套模型SubItem同样会被拆分为-Input/-Output两份并且带有computed_field的模型即使关闭分离模式也会强制保留-Input/-Output两份 schema原因见下文源码分析。关闭 Schema 分离separate_input_output_schemasFalse有些场景下你可能希望输入和输出使用同一份 schema。最典型的使用场景是你已经有了一些基于旧版 OpenAPI 自动生成的客户端代码 / SDK暂时不想立刻更新它们虽然将来大概率会更新但也许不是现在。在这种情况下可以在FastAPI中通过参数separate_input_output_schemasFalse关闭这一特性。注对separate_input_output_schemas的支持是在 FastAPI0.102.0版本中添加的。完整示例见 tutorial002_py310.py与 tutorial001 唯一的不同就在第 10 行from fastapi import FastAPI from pydantic import BaseModel class Item(BaseModel): name: str description: str | None None app FastAPI(separate_input_output_schemasFalse) app.post(/items/) def create_item(item: Item): return item app.get(/items/) def read_items() - list[Item]: return [ Item( namePortal Gun, descriptionDevice to travel through the multi-rick-verse, ), Item(namePlumbus), ]设置该参数后OpenAPI 中就不再为模型生成Item-Input/Item-Output而只有唯一的一份Itemschema其中description被标记为非必填即采用输入侧的较宽松语义。该测试快照同样来自 test_openapi_separate_input_output_schemas.py 中test_openapi_schema_no_separateItem: { type: object, title: Item, required: [name], properties: { name: {type: string, title: Name}, description: {anyOf: [{type: string}, {type: null}], title: Description}, sub: {anyOf: [{$ref: #/components/schemas/SubItem}, {type: null}]} } }请求体与响应体引用的都是#/components/schemas/Item嵌套模型SubItem也同理只保留一份。值得强调的是关闭分离模式只影响 OpenAPI 文档的生成不改变 API 的实际运行时行为。上述测试中test_create_item、test_read_items等用例都断言了分离模式与非分离模式下响应体完全一致均返回description: None等带默认值的完整数据即服务端序列化逻辑不受影响被调整的只是文档层面的 required 语义。源码级实现参数从哪来、在哪起作用1. 参数定义与存储FastAPI 应用构造器在 applications.py 中FastAPI.__init__声明了该参数约第 780–813 行默认值为True其官方文档字符串本身就解释了语义separate_input_output_schemas: Annotated[ bool, Doc( Whether to generate separate OpenAPI schemas for request body and response body when the results would be more precise. This is particularly useful when automatically generating clients. ... ), ] True,构造完成后该值被保存到实例属性第 890 行self.separate_input_output_schemas separate_input_output_schemas2. 参数传递从 app.openapi() 到 get_openapi()当首次访问/openapi.json时app.openapi()方法applications.py会调用get_openapi(...)并透传该参数第 1099 行self.openapi_schema get_openapi( titleself.title, versionself.version, ... separate_input_output_schemasself.separate_input_output_schemas, external_docsself.openapi_external_docs, )随后在 openapi/utils.py 中separate_input_output_schemas沿着 OpenAPI 生成的整条调用链层层透传get_openapi()→ 为每个路径生成请求体 / 响应 / 回调 / Webhook schema 的各函数如_get_openapi_operation_parameters、get_openapi_operation_request_body等函数签名默认值均为True最终在生成components/schemas定义时交给 Pydantic 兼容层。3. 核心判定逻辑何时按输入生成真正的行为分叉点在 fastapi/_compat/v2.py 的get_definitions中约第 324–335 行inputs [ ( field, ( field.mode if (separate_input_output_schemas or _has_computed_fields(field)) else validation ), field._type_adapter.core_schema, ) for field in list(fields) list(unique_flat_model_fields) ] field_mapping, definitions schema_generator.generate_definitions(inputsinputs)这段代码的含义是分离模式开启默认每个ModelField保持自身的modevalidation对应输入serialization对应输出交给 Pydantic v2 的GenerateJsonSchema.generate_definitions分别生成两种模式的 schema。Pydantic 在序列化模式下会自动把有默认值的字段纳入required并附加-Output后缀输入校验模式下则把默认值字段视为可选附加-Input后缀。分离模式关闭所有字段被强制统一改写为validation模式即else validation分支于是整个应用只按输入侧的宽松语义生成唯一一份 schemarequired只包含真正没有默认值的字段。特例——计算字段_has_computed_fields(field)会检查该模型的 Pydantic core schema 中是否存在computed_fieldsfastapi/_compat/v2.py。如果存在即使关闭分离模式该模型也仍然保留双 schema。原因从源码结构可以推断computed_field只在序列化时产生其属性带有readOnly: True输入 schema 中不包含它输出 schema 中才包含它若强行合并会丢失字段这一点在测试test_with_computed_field及两份 OpenAPI 快照中均可验证——非分离模式下Item合并了但WithComputedField-Input/WithComputedField-Output依然存在。同理get_schema_from_model_fieldfastapi/_compat/v2.py在把字段解析为$ref时也遵循同一个三元判定override_mode: Literal[validation] | None ( None if (separate_input_output_schemas or _has_computed_fields(field)) else validation )即关闭分离模式时从字段映射中取 schema 的键被强制指向validation那一侧。4. 相关的 Pydantic 侧配置在 test_openapi_separate_input_output_schemas.py 中可以看到模型上设置了model_config {json_schema_serialization_defaults_required: True}。这是 Pydantic v2 的模型配置项控制序列化模式下的 schema 是否将有默认值的字段标记为 required——它正是本文开头所述输出 schema 中默认值字段变成 required这一行为的 Pydantic 侧开关与 FastAPI 侧的separate_input_output_schemas决定是否生成两份schema是两个不同层面、相互配合的配置前者决定输出语义后者决定文档中呈现几份 schema。实践建议与适用边界结合文档与源码可以归纳出如下实践指引新项目建议保持默认separate_input_output_schemasTrue。输入输出分离的文档对人工阅读和自动生成客户端都更精确尤其是响应体字段永远存在但可能为 null的语义只有-Outputschema 能如实表达。存量客户端未同步前可临时关闭。当你已有基于旧版 OpenAPI 生成的客户端 / SDK且希望 OpenAPI 结构与旧版单 schema、宽松 required保持兼容时传入FastAPI(separate_input_output_schemasFalse)即可平滑过渡后续更新客户端时可再切回默认值。注意 computed_field 的例外。从源码结构看只要模型包含computed_field即使设置了separate_input_output_schemasFalse该模型依然会生成-Input/-Output两份 schema这是由计算字段只存在于序列化侧的客观限制决定的属于预期行为而非 bug。只影响文档不影响运行行为。该参数仅改变/openapi.json的components/schemas结构请求校验、响应序列化的运行时逻辑不受影响测试中对两种模式响应体的逐字节断言可作为证据。版本前提该参数自 FastAPI0.102.0起可用本文所有行为描述基于当前仓库源码适用于 Pydantic v2 环境。相关资源文档示例源码docs_src/separate_openapi_schemas/tutorial001_py310.py、docs_src/separate_openapi_schemas/tutorial002_py310.py行为验证测试tests/test_openapi_separate_input_output_schemas.py、tests/test_computed_fields.py核心实现fastapi/applications.py、fastapi/openapi/utils.py、fastapi/_compat/v2.py原始文档docs/en/docs/how-to/separate-openapi-schemas.md【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考