第四章 自动交互式文档
本章目标:掌握 FastAPI 内置的两种 API 文档(Swagger UI / ReDoc),学会通过代码优化文档展示效果,让 API 文档更专业、更好用。
预计时长:20 分钟
4.1 Swagger UI(/docs)
启动任何 FastAPI 应用后,访问 http://127.0.0.1:8000/docs 即可打开 Swagger UI。
它能做什么
Swagger UI 功能一览:
┌─────────────────────────────────────────────┐
│ Swagger UI (http://127.0.0.1:8000/docs) │
├─────────────────────────────────────────────┤
│ ✅ 列出所有接口(路径 + 方法) │
│ ✅ 展示每个接口的参数说明 │
│ ✅ 在线填写参数,点击 Execute 直接调用 │
│ ✅ 查看真实的请求 URL、请求头、请求体 │
│ ✅ 查看响应状态码、响应体、响应头 │
│ ✅ 展示请求/响应的 JSON Schema │
│ ✅ 支持 Bearer Token 等认证方式 │
└─────────────────────────────────────────────┘在线测试 API
准备一个示例应用 chapter04.py:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI(title="第四章示例 API", version="1.0.0")
class Item(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0)
quantity: int = Field(ge=0, default=0)
items_db: dict[int, dict] = {}
next_id = 1
@app.post("/items", status_code=201)
def create_item(item: Item):
global next_id
new_item = {"id": next_id, **item.model_dump()}
items_db[next_id] = new_item
next_id += 1
return new_item
@app.get("/items/{item_id}")
def get_item(item_id: int):
if item_id not in items_db:
return {"error": "Item not found"}
return items_db[item_id]
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
all_items = list(items_db.values())
return all_items[skip : skip + limit]fastapi dev chapter04.py打开 http://127.0.0.1:8000/docs ,操作流程:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 找到 POST /items | 绿色标签表示 POST 方法 |
| 2 | 点击展开 → 点击 Try it out | 进入编辑模式 |
| 3 | 在 Request body 中填写 JSON | {"name": "键盘", "price": 299, "quantity": 5} |
| 4 | 点击 Execute | 发起真实请求 |
| 5 | 查看下方 Response body | 看到返回的 JSON(含自动分配的 id) |
查看请求/响应 Schema
在 Swagger UI 页面底部有一个 Schemas 区域,展示了所有 Pydantic 模型的 JSON Schema:
Schemas 区域展示:
┌──────────────────────────────┐
│ Item │
│ ├── name* string │
│ │ minLength: 1 │
│ │ maxLength: 100 │
│ ├── price* number │
│ │ exclusiveMinimum: 0 │
│ └── quantity integer │
│ minimum: 0 │
│ default: 0 │
└──────────────────────────────┘每个接口展开后也能切换查看:
| 标签 | 内容 |
|---|---|
| Schema | 字段名、类型、约束、是否必填 |
| Example Value | 自动生成的示例 JSON |
这些信息全部来自你的 Pydantic 模型定义和 Field() 参数,零额外配置。
4.2 ReDoc(/redoc)
访问 http://127.0.0.1:8000/redoc 即可打开 ReDoc 文档。
Swagger UI vs ReDoc
| 维度 | Swagger UI(/docs) | ReDoc(/redoc) |
|---|---|---|
| 交互性 | 可直接发请求测试 | 只读,不能发请求 |
| 布局 | 单栏,逐个展开 | 三栏,左侧导航 + 中间说明 + 右侧示例 |
| 适合谁 | 开发者自己调试 | 分享给前端 / 第三方 / 写文档 |
| 信息密度 | 适中 | 更高,一屏看到更多信息 |
| 美观度 | 功能优先 | 排版更专业 |
适合什么场景
开发阶段 → Swagger UI(/docs)
├── 自己调试接口
├── 在线修改参数测试
└── 快速验证返回值
交付阶段 → ReDoc(/redoc)
├── 发给前端同事看接口定义
├── 发给第三方对接
└── 作为项目 API 文档存档提示:两个文档页面基于同一份 OpenAPI Schema(
/openapi.json),内容完全一致,只是展现形式不同。
4.3 如何优化文档展示
默认文档已经很好用了,但通过几个简单的参数可以让文档更专业。
路由参数:summary、description、tags
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI(
title="商品管理 API",
description="一个简单的商品 CRUD 示例,用于演示文档优化。",
version="2.0.0",
)
class ItemCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="商品单价(元)")
quantity: int = Field(ge=0, default=0, description="库存数量")
class ItemResponse(BaseModel):
id: int
name: str
price: float
quantity: int
items_db: dict[int, dict] = {}
next_id = 1
@app.post(
"/items",
response_model=ItemResponse,
status_code=201,
tags=["商品管理"],
summary="创建商品",
description="传入商品名称、价格和库存数量,创建一条新商品记录。返回包含自动分配 ID 的完整商品信息。",
)
def create_item(item: ItemCreate):
global next_id
new_item = {"id": next_id, **item.model_dump()}
items_db[next_id] = new_item
next_id += 1
return new_item
@app.get(
"/items/{item_id}",
response_model=ItemResponse,
tags=["商品管理"],
summary="获取单个商品",
description="根据商品 ID 获取商品详情。",
)
def get_item(item_id: int):
from fastapi import HTTPException
if item_id not in items_db:
raise HTTPException(status_code=404, detail="商品不存在")
return items_db[item_id]
@app.get(
"/items",
response_model=list[ItemResponse],
tags=["商品管理"],
summary="获取商品列表",
description="分页获取所有商品,支持 `skip` 和 `limit` 查询参数。",
)
def list_items(skip: int = 0, limit: int = 10):
all_items = list(items_db.values())
return all_items[skip : skip + limit]
@app.get(
"/health",
tags=["系统"],
summary="健康检查",
)
def health_check():
return {"status": "ok"}各参数的作用:
| 参数 | 出现位置 | 效果 |
|---|---|---|
tags=["商品管理"] | 文档左侧分组 / Swagger UI 折叠分组 | 接口按标签归类,一目了然 |
summary="创建商品" | 接口标题(折叠状态下可见) | 替代默认的函数名 |
description="..." | 接口展开后的详细说明 | 支持 Markdown 格式 |
response_model=ItemResponse | Schema 区域 | 明确展示返回数据结构 |
效果对比:
优化前(默认): 优化后:
┌──────────────────────┐ ┌──────────────────────┐
│ default │ │ 商品管理 │
│ GET /items │ │ POST /items 创建商品│
│ POST /items │ │ GET /items 商品列表│
│ GET /items/{id} │ │ GET /items/{id} │
│ GET /health │ │ │
└──────────────────────┘ │ 系统 │
│ GET /health 健康检查│
└──────────────────────┘Pydantic Model 的 json_schema_extra(示例值)
默认情况下,Swagger UI 会根据字段类型自动生成示例值("string"、0 等),不太直观。通过 model_config 可以提供更有意义的示例:
from pydantic import BaseModel, Field
class ItemCreate(BaseModel):
model_config = {
"json_schema_extra": {
"examples": [
{
"name": "机械键盘",
"price": 399.0,
"quantity": 50,
}
]
}
}
name: str = Field(min_length=1, max_length=100, description="商品名称")
price: float = Field(gt=0, description="商品单价(元)")
quantity: int = Field(ge=0, default=0, description="库存数量")也可以在 Field() 级别单独设置示例值:
class ItemCreate(BaseModel):
name: str = Field(
min_length=1,
max_length=100,
description="商品名称",
examples=["机械键盘"],
)
price: float = Field(
gt=0,
description="商品单价(元)",
examples=[399.0],
)
quantity: int = Field(
ge=0,
default=0,
description="库存数量",
examples=[50],
)两种方式的对比:
| 方式 | 粒度 | 适合场景 |
|---|---|---|
model_config → json_schema_extra | 整个模型级别 | 提供一组完整的示例数据 |
Field(examples=[...]) | 单个字段级别 | 各字段独立展示示例 |
接口分组与排序
接口在文档中的分组由 tags 控制,排序由 tag 出现的先后顺序 和 路由注册顺序 决定。
如果想精确控制 tag 的顺序和描述,在 FastAPI() 实例中配置 openapi_tags:
from fastapi import FastAPI
tags_metadata = [
{
"name": "商品管理",
"description": "商品的增删改查接口",
},
{
"name": "订单管理",
"description": "订单相关操作",
},
{
"name": "系统",
"description": "健康检查、版本信息等系统级接口",
},
]
app = FastAPI(
title="商品管理系统",
version="2.0.0",
openapi_tags=tags_metadata,
)文档中 tag 的显示顺序 = openapi_tags 列表中的定义顺序,同一 tag 下的接口按路由注册顺序排列。
完整优化示例
把以上所有优化技巧整合到一个完整可运行的文件 chapter04_full.py:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
tags_metadata = [
{"name": "商品管理", "description": "商品的增删改查操作"},
{"name": "系统", "description": "系统级接口"},
]
app = FastAPI(
title="商品管理系统 API",
description="FastAPI 文档优化示例,演示 tags / summary / description / json_schema_extra 的效果。",
version="2.0.0",
openapi_tags=tags_metadata,
)
# ---------- 数据模型 ----------
class ItemCreate(BaseModel):
model_config = {
"json_schema_extra": {
"examples": [
{
"name": "机械键盘",
"price": 399.0,
"quantity": 50,
}
]
}
}
name: str = Field(min_length=1, max_length=100, description="商品名称")
price: float = Field(gt=0, description="商品单价(元)")
quantity: int = Field(ge=0, default=0, description="库存数量")
class ItemResponse(BaseModel):
id: int = Field(description="商品 ID")
name: str = Field(description="商品名称")
price: float = Field(description="商品单价(元)")
quantity: int = Field(description="库存数量")
# ---------- 模拟存储 ----------
items_db: dict[int, dict] = {}
next_id = 1
# ---------- 接口 ----------
@app.post(
"/items",
response_model=ItemResponse,
status_code=201,
tags=["商品管理"],
summary="创建商品",
description="传入商品信息,创建一条新记录并返回完整数据(含自动分配的 ID)。",
)
def create_item(item: ItemCreate):
global next_id
new_item = {"id": next_id, **item.model_dump()}
items_db[next_id] = new_item
next_id += 1
return new_item
@app.get(
"/items",
response_model=list[ItemResponse],
tags=["商品管理"],
summary="获取商品列表",
description="分页获取所有商品。`skip` 跳过前 N 条,`limit` 限制返回数量。",
)
def list_items(skip: int = 0, limit: int = 10):
return list(items_db.values())[skip : skip + limit]
@app.get(
"/items/{item_id}",
response_model=ItemResponse,
tags=["商品管理"],
summary="获取单个商品",
description="根据商品 ID 查询详情,不存在时返回 404。",
)
def get_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="商品不存在")
return items_db[item_id]
@app.delete(
"/items/{item_id}",
tags=["商品管理"],
summary="删除商品",
description="根据商品 ID 删除记录,不存在时返回 404。",
)
def delete_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail="商品不存在")
del items_db[item_id]
return {"message": "删除成功"}
@app.get(
"/health",
tags=["系统"],
summary="健康检查",
description="返回服务运行状态,可用于负载均衡器探活。",
)
def health_check():
return {"status": "ok", "version": "2.0.0"}fastapi dev chapter04_full.py启动后分别打开 /docs 和 /redoc,观察:
- 接口按「商品管理」「系统」分组
- 每个接口有清晰的中文标题和描述
- 请求体有真实的示例数据(机械键盘 / 399 / 50)
- 响应体展示了精确的 Schema
4.4 动手练习
练习 1:给第三章的 Todo API 添加文档优化
回到之前的 Todo 应用,完成以下优化:
- 给
FastAPI()添加title、description、version - 给每个路由添加
tags、summary、description - 给
TodoCreate模型添加json_schema_extra示例值 - 定义
openapi_tags控制分组顺序
参考效果:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
tags_metadata = [
{"name": "Todo", "description": "待办事项的增删改查"},
]
app = FastAPI(
title="Todo API",
description="一个支持数据校验的待办事项管理接口",
version="1.0.0",
openapi_tags=tags_metadata,
)
class TodoCreate(BaseModel):
model_config = {
"json_schema_extra": {
"examples": [
{
"title": "学习 FastAPI 文档优化",
"description": "掌握 tags、summary、json_schema_extra 的用法",
"priority": 4,
}
]
}
}
title: str = Field(min_length=1, max_length=200, description="待办事项标题")
description: str | None = Field(default=None, max_length=500, description="详细说明")
priority: int = Field(ge=1, le=5, default=3, description="优先级 1~5,数字越大越紧急")
@field_validator("title")
@classmethod
def title_must_not_be_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError("标题不能全是空格")
return v.strip()
class TodoResponse(BaseModel):
id: int = Field(description="自动分配的唯一 ID")
title: str
description: str | None
priority: int
completed: bool
todos: dict[int, dict] = {}
next_id = 1
@app.post("/todos", response_model=TodoResponse, status_code=201,
tags=["Todo"], summary="创建待办")
def create_todo(todo: TodoCreate):
global next_id
new_todo = {"id": next_id, "completed": False, **todo.model_dump()}
todos[next_id] = new_todo
next_id += 1
return new_todo
@app.get("/todos", response_model=list[TodoResponse],
tags=["Todo"], summary="获取待办列表")
def list_todos():
return list(todos.values())
@app.get("/todos/{todo_id}", response_model=TodoResponse,
tags=["Todo"], summary="获取单个待办")
def get_todo(todo_id: int):
if todo_id not in todos:
raise HTTPException(status_code=404, detail="Todo 不存在")
return todos[todo_id]练习 2:自定义文档 URL
FastAPI 允许修改文档的访问路径,甚至关闭文档:
app = FastAPI(
docs_url="/swagger", # Swagger UI 改为 /swagger
redoc_url="/api-doc", # ReDoc 改为 /api-doc
)
# 生产环境关闭文档:
# app = FastAPI(docs_url=None, redoc_url=None)试试修改后访问新路径,确认文档正常显示。
本章小结
| 概念 | 要点 |
|---|---|
| Swagger UI | /docs,可在线测试接口,开发阶段首选 |
| ReDoc | /redoc,三栏布局,适合分享给前端/第三方 |
| OpenAPI Schema | /openapi.json,两种文档的数据源,自动生成 |
tags | 接口分组,在文档中按标签归类展示 |
summary | 接口标题,折叠时可见 |
description | 接口详细说明,支持 Markdown |
json_schema_extra | 为 Pydantic 模型提供完整示例值 |
Field(examples=[...]) | 为单个字段提供示例值 |
openapi_tags | 控制 tag 顺序和描述 |
docs_url / redoc_url | 自定义或关闭文档路径 |
下一章预告:我们将学习 FastAPI 最强大的特性之一——依赖注入系统。它能帮你把公共逻辑(数据库连接、用户认证、权限校验)优雅地抽离出来,让路由函数保持简洁。