FastAPI 如何根据类型注解生成 OpenAPI 文档?类型注解在运行时起什么作用?
简化版
FastAPI 会读取路径函数的类型注解、默认值、Path、Query、Body、Pydantic 模型等信息,用它们完成参数解析、数据校验、序列化和 OpenAPI 文档生成。普通 Python 类型注解本身不强制运行时类型,但 FastAPI 主动读取这些注解,并交给 Pydantic 等组件在运行时执行校验。
详细版
FastAPI 的核心特点之一是“类型注解即接口契约”。比如参数写成 item_id: int,FastAPI 会知道它应该把路径里的字符串转成整数;请求体写成 Pydantic 模型,FastAPI 会根据模型字段生成 JSON Schema;响应模型也能用于输出过滤和文档展示。
from pydantic import BaseModel
from fastapi import FastAPI, Query
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items/{item_id}", response_model=Item)
def update_item(item_id: int, q: str | None = Query(None), item: Item = ...):
return item
这段代码会同时影响运行时行为和文档:item_id 被解析为整数,q 被识别为查询参数,item 被识别为请求体,Item 模型会变成 OpenAPI schema。面试要说清:Python 注解只是元数据,FastAPI 利用这些元数据构建了运行时校验和文档生成机制。
完整版教学
一、为什么 FastAPI 特别强调类型注解
传统 Web 框架里,参数通常先以字符串形式进入视图函数,开发者再手动转换和校验。接口多了以后,参数解析、错误响应、接口文档和实际代码很容易不一致。FastAPI 的设计是把函数签名变成接口契约的来源。
例如:
@app.get("/users/{user_id}")
def get_user(user_id: int, active: bool = True):
...
这行签名表达了三件事:user_id 来自路径并应是整数;active 有默认值,通常来自查询参数;接口文档可以展示这两个参数的类型和默认值。代码、校验和文档共享同一份信息,减少重复维护。
函数签名
|
+-> 参数来源推断
+-> Pydantic 校验
+-> OpenAPI Schema
+-> Swagger UI / ReDoc
二、Python 注解本身并不强制类型
普通 Python 不会因为你写了 x: int 就禁止传字符串。类型注解默认是元数据,主要给静态检查器、IDE、框架或运行时库读取。FastAPI 的关键是主动读取注解,并在请求进入时执行转换和校验。
def add(x: int, y: int):
return x + y
add("1", "2") # 普通 Python 会返回 "12",不会自动报错
但在 FastAPI 路径函数里:
@app.get("/add")
def add(x: int, y: int):
return {"result": x + y}
请求 /add?x=1&y=2 会得到整数加法;请求 /add?x=abc&y=2 会返回 422 校验错误。差异来自 FastAPI/Pydantic 的运行时处理,不是 Python 解释器自己强制了注解。
三、FastAPI 如何推断参数来源
FastAPI 会根据路径模板、参数类型和默认值判断参数来自哪里。路径模板里出现的参数来自 path;简单类型且不在路径里的参数通常来自 query;Pydantic 模型通常来自 body;也可以用 Path、Query、Header、Cookie、Body 显式声明。
| 写法 | 参数来源 | 示例 |
|---|---|---|
/users/{user_id} + user_id: int | Path | /users/123 |
| `q: str | None = None` | Query |
token: str = Header() | Header | token: xxx |
item: Item | Body | JSON 请求体 |
limit: int = Query(10, ge=1, le=100) | Query + 约束 | 分页限制 |
from fastapi import Query
@app.get("/search")
def search(q: str, page: int = Query(1, ge=1), size: int = Query(20, le=100)):
...
这段签名不仅能校验 page >= 1 和 size <= 100,还会把这些约束写入 OpenAPI 文档。
四、Pydantic 模型如何变成 Schema
请求体和响应体常用 Pydantic 模型描述。模型字段的类型、默认值、约束、描述会被转成 JSON Schema,再组合进 OpenAPI 文档。Swagger UI 看到的请求体示例和字段说明,大多来自这些模型信息。
from pydantic import BaseModel, Field
class CreateUser(BaseModel):
name: str = Field(min_length=2, max_length=30)
age: int = Field(ge=0, le=150)
生成的 schema 会包含类似约束:
name: string, minLength=2, maxLength=30
age: integer, minimum=0, maximum=150
数字化理解:如果 10 个接口都使用 CreateUser,字段规则只要在模型里维护一份。文档、校验和编辑器提示都会跟着变化,这就是 FastAPI 比手写文档更不容易漂移的原因。
五、response_model 为什么不只是文档
response_model 不只影响 OpenAPI,也会影响运行时响应序列化和字段过滤。比如数据库对象里有 password_hash,响应模型没声明这个字段,FastAPI 输出时会按响应模型过滤,避免敏感字段泄露。
class UserOut(BaseModel):
id: int
name: str
@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int):
return {"id": user_id, "name": "Ada", "password_hash": "secret"}
最终响应只应包含 id 和 name。这也是工程上推荐区分 UserCreate、UserUpdate、UserOut 的原因:输入、更新和输出的字段语义不同,不能偷懒用同一个模型包打天下。
response_model是接口出口的闸门,不只是给 Swagger 好看。
六、OpenAPI 自动文档的边界
自动文档的质量取决于类型和模型写得是否准确。如果接口参数都写成 dict、Any,或者返回值没有响应模型,FastAPI 仍能生成文档,但文档会很粗,校验能力也会变弱。自动生成不是自动设计。
@app.post("/bad")
def bad(payload: dict):
...
这类接口对调用方只说明“传一个对象”,字段结构、必填项、范围都不清楚。更好的做法是定义清晰模型:
class PayRequest(BaseModel):
order_id: str
amount: int = Field(gt=0)
此外,OpenAPI 文档是接口契约的一部分,涉及鉴权、错误码、分页格式、业务状态码时,也要用统一响应模型和异常处理补齐,不要只依赖默认生成。
七、常见误区与追问
- 误区:Python 类型注解本身会在运行时强制校验。 普通 Python 不会强制,是 FastAPI/Pydantic 主动读取注解并执行校验。
- 误区:自动文档一定准确。 文档准确性取决于函数签名、模型、响应模型和错误响应是否认真设计。
- 误区:
response_model只是文档展示。 它还会影响响应序列化和字段过滤,是防止多返回字段的重要手段。 - 追问:路径参数和查询参数怎么区分? 路径模板里出现的是 path 参数;简单类型且不在路径里的通常是 query 参数。
- 追问:请求体为什么常用 Pydantic 模型? 模型能表达字段类型、约束、默认值,并生成 JSON Schema。
- 追问:
Any和dict有什么问题? 校验弱、文档粗、调用方不知道结构,接口契约容易漂移。 - 追问:FastAPI 的 422 是怎么来的? 参数解析或 Pydantic 校验失败时,FastAPI 默认返回 422 Unprocessable Entity。
八、加强记忆
FastAPI 的类型注解要记成“接口契约的源头”:它不靠 Python 解释器强制类型,而是框架读取注解,推断参数来源,交给 Pydantic 校验,再生成 OpenAPI 文档和响应序列化规则。写得清楚,校验、文档、IDE、调用方都受益;写成 dict/Any,自动文档也只能生成一团模糊影子。面试回答时抓住“注解是元数据,FastAPI 把元数据变成运行时行为”这条主线就够稳。