ARTICLE · 1025693
FastAPI入门(5):自动文档与调试技巧
本章介学习 FastAPI 生成的 OpenAPI 文档,以及日常联调时常用的排查方法。
1 场景引入
前面的学习之后,写的接口已经能用了,假如现在前端同事来找你:
"你这接口返回什么字段?
priority有哪些值?哪些字段必填?错误码都有哪些?"
你有两个选择:
打开聊天窗口,一个个截图、一段段描述——每次改代码都要重新同步一遍。
让他打开
http://your-host/docs。
自动文档能减少手工同步字段和参数的工作,但它只反映声明出来的契约;自定义响应和错误结构仍要主动维护。
2 最小可运行示例
默认文档已经很好了,但缺少业务语义。我们可以自己加上描述信息:
from enum import Enum
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
tags_metadata = [
{
"name": "todos",
"description": "待办事项的增删改查。**所有接口都需要认证。**",
},
{
"name": "system",
"description": "系统状态与健康检查,无需认证。",
},
]
app = FastAPI(
title="Todo API",
description="""
## 简介
待办事项管理服务的后端接口。
## 认证
除 `/health` 外所有接口需要在 Header 中携带 Token:
~~~
Authorization: Bearer <your-token>
~~~
## 错误响应
所有错误统一返回:
~~~json
{
"code": "ERROR_CODE",
"message": "可读的错误说明",
"errors": []
}
~~~
""",
version="1.0.0",
contact={"name": "API 支持", "email": "api@example.com"},
openapi_tags=tags_metadata,
)
classPriority(str, Enum):
low = "low"
medium = "medium"
high = "high"
classTodoCreate(BaseModel):
title: str = Field(
min_length=1,
max_length=100,
description="待办标题,1-100 字符,不能全为空白字符",
examples=["学习 FastAPI 依赖注入"],
)
priority: Priority = Field(
default=Priority.medium,
description="优先级,默认 medium",
)
classTodo(BaseModel):
id: int = Field(description="待办唯一 ID", examples=[1])
title: str
priority: Priority
done: bool = Field(default=False, description="是否已完成")
classErrorResponse(BaseModel):
code: str = Field(description="业务错误码")
message: str = Field(description="错误说明")
errors: list[dict] = Field(default_factory=list, description="字段级错误明细")
todos_db: dict[int, Todo] = {1: Todo(id=1, title="写教程", priority=Priority.high)}
@app.get(
"/health",
tags=["system"],
summary="健康检查",
description="用于负载均衡探活和监控系统检查服务是否存活。",
)
defhealth():
return {"status": "ok", "version": "1.0.0"}
@app.get(
"/todos",
tags=["todos"],
summary="列出待办",
response_model=list[Todo],
responses={
401: {"model": ErrorResponse, "description": "未认证"},
500: {"model": ErrorResponse, "description": "服务器内部错误"},
},
)
deflist_todos():
"""返回当前用户的全部待办事项,按创建时间倒序排列。"""
returnlist(todos_db.values())
@app.post(
"/todos",
tags=["todos"],
summary="创建待办",
status_code=status.HTTP_201_CREATED,
response_model=Todo,
responses={
409: {"model": ErrorResponse, "description": "标题重复"},
},
)
defcreate_todo(payload: TodoCreate):
"""
创建一个新待办。
- **title**: 必填,1-100 字符
- **priority**: 可选,默认 medium
"""
ifany(t.title == payload.title for t in todos_db.values()):
raise HTTPException(
status_code=409,
detail={"code": "TITLE_DUPLICATED", "message": "该标题已存在", "errors": []},
)
new_id = max(todos_db.keys(), default=0) + 1
todo = Todo(id=new_id, **payload.model_dump())
todos_db[new_id] = todo
return todo
@app.get(
"/todos/{todo_id}",
tags=["todos"],
summary="获取单个待办",
response_model=Todo,
deprecated=False,
responses={404: {"model": ErrorResponse, "description": "待办不存在"}},
)
defget_todo(todo_id: int):
if todo_id notin todos_db:
raise HTTPException(
status_code=404,
detail={"code": "TODO_NOT_FOUND", "message": f"待办 {todo_id} 不存在", "errors": []},
)
return todos_db[todo_id]打开 /docs,现在你看到的是一份有标题、有简介、有分组、有错误码说明、有字段描述和示例值的完整文档。
3 原理讲解
3.1 /docs 与 /redoc 的区别
FastAPI 默认提供两个文档页面:
/docs | 开发调试、联调 | ||
/redoc | 交付给前端当手册 |
两者数据源相同,都是 /openapi.json。
curl http://127.0.0.1:8000/openapi.json这份 JSON 就是 OpenAPI 规范文档。它的价值在于可以用工具链二次利用:
生成前端 TypeScript 类型定义
生成客户端 SDK
导入 Postman / Apifox
接入 API 网关做校验
所以你在代码里写的 description、Field 描述、responses,最终不只是给人看的,也是给机器读的。
3.2 文档配置的层级结构
FastAPI 的文档信息分三层,各管各的:
应用级(FastAPI(...) 参数)
├── title / description / version / contact
└── openapi_tags → 定义分组说明
│
路由级(装饰器参数)
├── tags → 归到哪个分组
├── summary → 一眼可见的短标题
├── description → 详细说明(支持 Markdown)
└── responses → 声明额外的错误响应
│
字段级(Field / Query 参数)
└── description / examples → 字段说明与示例值Markdown 支持:应用的 description、接口的 description、Pydantic 模型的 description、以及函数的 docstring,都支持 Markdown 语法。
其中函数的 docstring 会自动成为接口的 description(如果装饰器里没显式传 description):
@app.get("/todos")
deflist_todos():
"""
返回当前用户的全部待办事项。
- **排序**:按创建时间倒序
- **分页**:默认最多返回 100 条
"""这段 docstring 会原样出现在文档里,Markdown 渲染生效。这是最省事的写文档方式:在函数里写注释,文档自动更新。
3.3 用 examples 提供真实示例值
文档里自动生成的示例通常是 "string"、0 这种占位符,对读者没帮助。三种方式提供真实示例:
**方式一:字段级 **examples
classTodoCreate(BaseModel):
title: str = Field(examples=["学习 FastAPI 依赖注入"])
priority: Priority = Field(default=Priority.medium)**方式二:模型级 **json_schema_extra
classTodoCreate(BaseModel):
title: str
priority: Priority = Priority.medium
model_config = {
"json_schema_extra": {
"examples": [
{"title": "写第三篇文章", "priority": "high"},
{"title": "复习依赖注入", "priority": "low"},
]
}
}**方式三:请求体级 **Body(examples=...)
如果需要在某个接口展示多个请求体示例,可以把示例写在 Body 上。路径操作装饰器没有通用的 examples 参数。
from typing import Annotated
from fastapi import Body
@app.post("/todos/example")
defcreate_example(
payload: Annotated[
TodoCreate,
Body(examples=[
{"title": "读 FastAPI 文档", "priority": "high"},
{"title": "整理笔记", "priority": "low"},
]),
],
):
return payload优先级:接口级 > 模型级 > 字段级。
3.4 responses 参数声明错误码
FastAPI 会自动列出成功响应和常见的请求校验错误,但业务错误(例如 409)需要自己声明:
@app.post(
"/todos",
responses={
409: {"model": ErrorResponse, "description": "标题重复"},
422: {"description": "参数校验失败(FastAPI 自动覆盖)"},
},
)
defcreate_todo(payload: TodoCreate):
...这样文档里 409 会展示完整的 ErrorResponse 结构,前端可以据此写错误处理。
FastAPI 会为请求校验自动加入 422 响应。通常不必重复声明它;如果应用注册了自定义校验异常处理器,也应同步检查 OpenAPI 中展示的 schema,避免文档与实际响应不一致。
4 深入与进阶用法
4.1 调试技巧一:用 /docs 直接发请求
Swagger UI 的 "Try it out" 是最快的调试方式,不用打开 Postman:
展开接口 → 点 "Try it out"
填参数 → 点 "Execute"
下方直接显示
curl命令、请求 URL、响应体、响应头
"Try it out" 生成的 curl 命令可以直接复制到终端或分享给同事——这个细节在联调时非常省事。
4.2 调试技巧二:热重载与日志
fastapi dev main.py热重载开启后,改代码保存即生效,不用手动重启。但有两类改动不会自动重载:
当前 conda 环境里新装的包(要重启进程)
环境变量变化(要重启进程)
日志方面,开发模式下请求日志默认输出到终端:
INFO: 127.0.0.1:52341 - "GET /todos HTTP/1.1" 200 OK想看更详细的请求信息(含请求体、耗时),可以用中间件自己打,或者加 uvicorn 的日志级别:
uvicorn main:app --reload --log-level debug4.3 调试技巧三:断言式调试法
FastAPI 的错误信息其实很精确,学会读就不需要到处打日志。
422 错误:看 loc 定位字段,看 msg 了解规则。
{
"detail":[{
"loc":["body","items",0,"price"],
"msg":"Input should be greater than 0",
"type":"greater_than"
}]
}上面 loc 的含义是:请求体中的 items 数组,第 0 个元素的 price 字段校验失败。
500 错误:终端里的 traceback 会精确指到出错行。常见原因:
ResponseValidationError | response_model |
AttributeError: 'NoneType' | |
TypeError: Object of type X is not JSON serializable |
FastAPI 自带的编码流程能处理 datetime、Decimal 等常用类型,但不认识任意自定义对象。对外接口最好声明 response_model;返回 ORM 对象时,再配合模型的 from_attributes=True(第 7 篇讲)。第三方类型则需要先转换成 JSON 可表示的数据,或提供自定义序列化规则。
4.4 生产环境如何保护文档
有时候我们需要关闭文档,是否关闭文档取决于部署场景。对于内部服务可以保留并加认证,公开 API 也可以保留只读文档,但不要把未经保护的敏感接口和 schema 暴露给所有人。
import os
app = FastAPI(
title="Todo API",
docs_url=Noneif os.getenv("ENV") == "production"else"/docs",
redoc_url=Noneif os.getenv("ENV") == "production"else"/redoc",
openapi_url=Noneif os.getenv("ENV") == "production"else"/openapi.json",
)如果决定关闭文档,需要同时处理 /docs、/redoc 和 /openapi.json,只关闭 /docs 并不会隐藏原始 schema。
如果既想保留文档又想安全,可以用 HTTP Basic 保护 /docs,或者只在内部网络开放。这部分第 16 篇部署时会展开。
4.5 用 openapi.json 生成前端类型
既然文档是结构化的 JSON,就可以自动化。Figma 之外的常见做法:
# 用 openapi-typescript 生成 TS 类型
npx openapi-typescript http://127.0.0.1:8000/openapi.json -o ./types/api.ts生成的类型可以直接用于前端请求库,接口改了重新生成一次即可。这是"文档驱动开发"的落地方式:后端改代码 → openapi.json 更新 → 前端类型自动同步,编译期就能发现不兼容的改动。
5 常见坑与报错解读
问题:/docs** 打开是空白页**
原因通常是 openapi.json 生成失败。检查浏览器控制台里对 /openapi.json 的请求:
如果返回 500:应用启动时有问题,看终端 traceback
如果 404:
openapi_url被禁用了
常见触发原因是模型定义有循环引用,或者用了 OpenAPI 无法表达的复杂类型。
问题:docstring 里的 Markdown 没渲染
检查格式:Markdown 需要空行分隔段落。另外 docstring 会成为 description,如果装饰器里也传了 description,装饰器的会覆盖 docstring。
问题:responses** 里的 422 和实际错误格式不一致**
如果自定义异常处理器改变了 422 的结构,应在 responses 中声明实际的响应模型,或自定义 OpenAPI schema;不要只写一段描述,让文档继续显示旧结构。
问题:文档里的示例值是 "string" 占位符,改不掉
检查是不是用了 Optional[str] 但没给 Field(examples=[...])。Pydantic v2 里字段级用的是 examples(复数,列表),不是 v1 的 example(单数)。
# Pydantic v2 正确写法
title: str = Field(examples=["真实示例值"])
# v1 老写法,v2 里不会报错但不生效
title: str = Field(example="真实示例值")问题:接口调整后 /docs 没更新
浏览器缓存。强制刷新(Ctrl+Shift+R)或直接访问 /openapi.json 确认后端数据已更新。
6 小结
FastAPI 的自动文档本质上是根据代码中的类型声明和元数据生成的接口契约,既方便开发阶段调试,也能降低前后端沟通成本。实际项目中,需要让文档与真实响应保持一致,并根据部署环境合理控制文档的访问范围。遇到联调问题时,先查看 OpenAPI 描述和错误信息,通常能更快找到原因。