FastAPI 如何从代码生成 OpenAPI Schema
前置知识
- ›OpenAPI 3.1.0 规范基础
- ›JSON Schema 基本概念
- ›Pydantic v2 Schema 生成机制
- ›第 2 篇:路由注册与参数自省
FastAPI 如何从代码生成 OpenAPI Schema
自动生成 API 文档是 FastAPI 最受欢迎的特性之一。只需编写带类型注解的 Python 函数,FastAPI 就能生成完整的 OpenAPI 3.1.0 规范——包括参数 Schema、请求体定义、响应模型、安全要求,甚至 SSE/JSONL 流式传输 Schema。生成的 Schema 既可驱动交互式 Swagger UI 和 ReDoc 页面,也可被代码生成工具用于生成带类型的客户端 SDK。
本文将完整追踪 Schema 生成流水线:从 FastAPI 类上的 openapi() 方法出发,经过路由遍历逻辑,再到通过 Pydantic 的 TypeAdapter 输出 JSON Schema,最终到达内嵌防 XSS JSON 的 HTML 文档页面。
懒加载与缓存机制
Schema 在首次访问时才会生成,并在之后被缓存复用:
fastapi/applications.py#L1068-L1099
flowchart TD
A["GET /openapi.json"] --> B["self.openapi()"]
B --> C{self.openapi_schema set?}
C -->|Yes| D["Return cached schema"]
C -->|No| E["get_openapi(title, version, routes, ...)"]
E --> F["Walk all routes"]
F --> G["Generate per-route schemas"]
G --> H["Collect $ref definitions"]
H --> I["Assemble OpenAPI document"]
I --> J["Cache in self.openapi_schema"]
J --> D
openapi() 方法会将工作委托给 fastapi/openapi/utils.py 中的 get_openapi() 函数,同时传入所有元数据(标题、版本、描述、标签、服务器信息)以及路由列表。生成结果会存入 self.openapi_schema,后续调用直接返回缓存值。
这意味着,如果在首次请求 Schema 之后再注册路由,新路由不会反映到 Schema 中——除非将 app.openapi_schema 设为 None 来手动清除缓存。实际上这种情况很少出现,因为路由通常在启动时就已注册完毕,但如果你在做动态路由注册,就需要留意这一点。
提示: 想要自定义生成的 Schema,可以在 FastAPI 子类中覆盖
app.openapi()方法,或在首次调用后直接修改app.openapi_schema。FastAPI 扩展 OpenAPI 的官方文档中涵盖了常见的定制模式。
从 Dependant 树到 OpenAPI 参数
对于每条路由,Schema 生成器会将 Dependant 树展平(使用与第 2 篇相同的 get_flat_dependant()),并将参数转换为 OpenAPI 参数对象:
fastapi/openapi/utils.py#L107-L177
flowchart TD
A["_get_openapi_operation_parameters(dependant)"] --> B["get_flat_dependant(skip_repeats=True)"]
B --> C["Group: path, query, header, cookie params"]
C --> D["For each param:"]
D --> E["get_schema_from_model_field(field)"]
E --> F["TypeAdapter.json_schema()"]
F --> G["Build OpenAPI parameter object"]
G --> H{Has examples?}
H -->|Yes| I["Add examples/example"]
H -->|No| J{Deprecated?}
I --> J
J -->|Yes| K["Add deprecated: true"]
J -->|No| L["Append to parameters list"]
K --> L
_get_flat_fields_from_params() 函数会按名称对参数去重,确保同一参数在依赖项和 endpoint 中都有声明时,OpenAPI 规范中只出现一次。get_flat_dependant() 中的 skip_repeats=True 标志在树级别处理了这一逻辑。
Header 参数有一个细节值得注意:当 convert_underscores 为 True(默认值)时,参数名 user_agent 在 OpenAPI 规范中会变为 user-agent。这样既符合 HTTP 头部名称使用连字符的惯例,又允许在代码中使用 Python 风格的下划线命名。
请求体 Schema 生成
请求体的 Schema 生成涉及四种不同情况:
fastapi/openapi/utils.py#L180-L212
- 单个 JSON body 参数:该字段的 Schema 直接作为请求体 Schema
- 多个 body 参数(嵌入模式):注册时由
get_body_field()创建一个合成 Pydantic 模型,并使用其 Schema - 表单数据:使用
Form()或File()FieldInfo 中的media_type(application/x-www-form-urlencoded或multipart/form-data) - 无请求体:返回
None,操作中不会出现requestBody字段
核心集成点是 get_schema_from_model_field(),它会调用 Pydantic 的 TypeAdapter 来生成 JSON Schema。separate_input_output_schemas 标志控制是否分别生成输入(校验)和输出(序列化)Schema——对于带有计算字段或不同序列化别名的模型尤为重要。
从依赖树提取安全定义
安全方案的提取之所以简洁,是因为安全方案本质上也是依赖项:
fastapi/openapi/utils.py#L81-L104
该函数遍历 _security_dependencies 缓存属性(用于从展平的依赖列表中筛选出 SecurityBase 实例),提取每个方案的 OpenAPI 模型,并收集 OAuth scope。来自同一安全方案、不同依赖项的 scope 会被合并。
这一过程在 get_openapi_path() 中按路由调用:
fastapi/openapi/utils.py#L286-L293
flowchart LR
A["Route with Security deps"] --> B["get_flat_dependant()"]
B --> C["._security_dependencies"]
C --> D["get_openapi_security_definitions()"]
D --> E["operation.security = [...]"]
D --> F["security_schemes updated"]
F --> G["Goes into components.securitySchemes"]
安全定义和操作级安全要求最终汇聚到顶层的 get_openapi() 函数,被组装进 OpenAPI 文档的 components.securitySchemes 部分。我们将在第 5 篇中深入探讨安全子系统本身。
流式传输 Schema 生成:SSE 与 JSONL
get_openapi_path() 对流式 endpoint 有专门处理,涵盖 SSE 和 JSONL 两种形式:
fastapi/openapi/utils.py#L356-L392
对于 JSONL 流,响应内容使用 application/jsonl,并通过 itemSchema 键描述单个数据项。对于 SSE 流,响应使用 text/event-stream,并采用 OpenAPI 3.2 规范中标准的 SSE 事件 Schema:
当通过返回类型注解(如 AsyncGenerator[Item, None])声明了流数据项类型时,SSE 事件 Schema 的 data 属性会补充 contentMediaType: "application/json" 以及指向该数据项 JSON Schema 的 contentSchema。
自定义 JSON Schema 与 bytes 修复
FastAPI 扩展了 Pydantic 的 GenerateJsonSchema,以修复一个特定的边界情况:
覆盖的 bytes_schema() 方法会生成带有 contentMediaType: "application/octet-stream" 的 Schema,并在配置了 base64 编码时额外添加 contentEncoding: "base64"。这一修复是必要的,因为 Pydantic 默认的 bytes Schema 不包含这些 OpenAPI 特有属性。代码中的 TODO 注释表明,一旦该修复被合并到上游,这部分代码将被移除。
文档 UI 生成与 XSS 防护
Swagger UI 和 ReDoc 的 HTML 页面会将 OpenAPI JSON 内嵌到 <script> 标签中。这带来了一个 XSS 风险:如果用户可控的数据进入了 OpenAPI 规范(例如通过描述字段),恶意的 </script> 标签就可能破坏 JSON 上下文,实现注入。
FastAPI 通过 _html_safe_json() 来防御这一问题:
fastapi/openapi/docs.py#L9-L19
def _html_safe_json(value: Any) -> str:
return (
json.dumps(value)
.replace("<", "\\u003c")
.replace(">", "\\u003e")
.replace("&", "\\u0026")
)
这段代码将 HTML 特殊字符替换为对应的 Unicode 转义序列,从根本上杜绝了内嵌 JSON 逃出 <script> 标签的可能。看似微小,却是不可或缺的安全措施。
校验错误 Schema
FastAPI 会自动为所有带有参数或请求体的操作添加 422 Unprocessable Entity 响应,其 Schema 引用 ValidationError 定义:
fastapi/openapi/utils.py#L42-L69
这些是与 Pydantic 校验错误格式相匹配的硬编码定义:一个由错误对象组成的数组,每个对象包含 loc(位置)、msg(消息)、type(错误类型)、input 以及 ctx(上下文)字段。它们与模型定义一同被添加到 components.schemas 部分。
提示: 可以通过在路由上设置自定义响应来抑制自动生成的 422 响应。如果你显式添加了
422、4XX或default响应,FastAPI 就不会再自动生成该响应。
Schema 完整组装流程
最后,get_openapi() 遍历所有路由,收集 Schema、定义和安全方案,并组装成最终的 OpenAPI 文档:
flowchart TD
A["get_openapi()"] --> B["Collect all model fields from all routes"]
B --> C["get_flat_models_from_fields()"]
C --> D["get_model_name_map()"]
D --> E["get_definitions() → JSON Schema for all models"]
E --> F["For each route: get_openapi_path()"]
F --> G["Collect: path ops, security schemes, definitions"]
G --> H["Merge all definitions"]
H --> I["Build OpenAPI dict with info, paths, components"]
I --> J["Validate with OpenAPI Pydantic model"]
J --> K["Return schema dict"]
在返回之前,Schema 会经过 FastAPI 自身的 OpenAPI Pydantic 模型进行验证,确保其结构合法。这样可以在 Schema 生成阶段就捕获错误,而不是等到文档工具消费规范时才暴露问题。
下一步
我们已经了解了安全定义如何从依赖树中提取并用于 OpenAPI 生成。但安全系统在请求处理时是如何工作的——OAuth scope 如何在嵌套依赖中传播,以及为什么依赖缓存键包含 scope——这些问题还有待探讨。这正是第 5 篇的主题。