Read OSS

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_schemaFastAPI 扩展 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_underscoresTrue(默认值)时,参数名 user_agent 在 OpenAPI 规范中会变为 user-agent。这样既符合 HTTP 头部名称使用连字符的惯例,又允许在代码中使用 Python 风格的下划线命名。

请求体 Schema 生成

请求体的 Schema 生成涉及四种不同情况:

fastapi/openapi/utils.py#L180-L212

  1. 单个 JSON body 参数:该字段的 Schema 直接作为请求体 Schema
  2. 多个 body 参数(嵌入模式):注册时由 get_body_field() 创建一个合成 Pydantic 模型,并使用其 Schema
  3. 表单数据:使用 Form()File() FieldInfo 中的 media_typeapplication/x-www-form-urlencodedmultipart/form-data
  4. 无请求体:返回 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:

fastapi/sse.py#L7-L17

当通过返回类型注解(如 AsyncGenerator[Item, None])声明了流数据项类型时,SSE 事件 Schema 的 data 属性会补充 contentMediaType: "application/json" 以及指向该数据项 JSON Schema 的 contentSchema

自定义 JSON Schema 与 bytes 修复

FastAPI 扩展了 Pydantic 的 GenerateJsonSchema,以修复一个特定的边界情况:

fastapi/_compat/v2.py#L44-L57

覆盖的 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 响应。如果你显式添加了 4224XXdefault 响应,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 篇的主题。