响应模型与状态码
写给初学者:每个概念都从”为什么需要它”开始讲,配合通俗比喻和完整可运行代码。
一、响应模型(Response Model)
1.1 为什么需要响应模型?
假设你的数据库里用户表有这些字段:id, username, email, password, is_admin, created_at
你查询用户后直接返回这个对象,客户端就会拿到密码和管理员权限——这是安全漏洞。
响应模型的作用:控制返回给客户端的数据里有哪些字段。
1.2 基本用法
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 这个模型用来接收请求(用户注册时需要传密码)
class UserCreate(BaseModel):
username: str
email: str
password: str # ← 需要接收密码
# 这个模型用来返回响应(返回给客户端时不能包含密码)
class UserOut(BaseModel):
id: int
username: str
email: str
# ← 注意:这里没有 password 字段!
@app.post("/users", response_model=UserOut) # ← 关键:指定响应模型
async def create_user(user: UserCreate):
# 模拟:从"数据库"拿到完整用户信息(包含密码)
db_user = {
"id": 1,
"username": user.username,
"email": user.email,
"password": user.password, # ← 数据库里有密码
}
return db_user
# 虽然返回了 password,但客户端收到的 JSON 里没有它!
# 因为 response_model=UserOut 会自动过滤掉 UserOut 里没定义的字段
客户端实际收到的数据:
{
"id": 1,
"username": "张三",
"email": "zhangsan@qq.com"
// ← 没有 password!被响应模型过滤掉了
}
1.3 响应模型的过滤机制
核心规则:响应模型里定义了什么字段,客户端就只能看到什么字段。
class UserOut(BaseModel):
id: int
username: str
@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int):
return {
"id": 1,
"username": "张三",
"email": "secret@qq.com", # ← 响应模型里没有,会被过滤
"password": "123456", # ← 响应模型里没有,会被过滤
"is_admin": True, # ← 响应模型里没有,会被过滤
"created_at": "2024-01-01" # ← 响应模型里没有,会被过滤
}
# 客户端只收到: {"id": 1, "username": "张三"}
1.4 response_model_exclude_unset=True
有时候你有可选字段,不想让没赋值的字段出现在返回数据里:
from typing import Optional
class UserOut(BaseModel):
id: int
username: str
nickname: Optional[str] = None # 可选字段,默认 None
bio: Optional[str] = None # 可选字段,默认 None
# 加上 exclude_unset:
@app.get("/users/{user_id}", response_model=UserOut, response_model_exclude_unset=True)
async def get_user(user_id: int):
return {"id": 1, "username": "张三"} # 只赋值了 id 和 username
# 返回: {"id": 1, "username": "张三"}
# ↑ nickname 和 bio 直接不出现在返回数据里
1.5 请求模型 vs 响应模型 —— 为什么要分开?
# ❌ 错误做法:一个模型既做请求又做响应
class User(BaseModel):
id: int # 请求时用户不可能知道自己的 id
username: str
password: str # 响应时不能返回密码
# ✅ 正确做法:分开定义
class UserCreate(BaseModel): # 请求用:注册时需要什么
username: str
password: str
class UserOut(BaseModel): # 响应用:返回时暴露什么
id: int
username: str
# 没有 password
通俗总结:
-
请求模型(UserCreate) = 点菜时你需要告诉服务员什么(菜名、数量、忌口)
-
响应模型(UserOut) = 服务员端上来什么(菜,但不会把厨房的配方告诉你)
二、响应类型(Response Types)
2.1 为什么需要自定义响应类型?
默认情况下,FastAPI 会把你返回的 dict/list 自动转成 JSON。但有时候你需要:
-
返回 HTML 页面(不是 JSON)
-
返回文件下载
-
自定义响应头(比如设置 Cookie)
-
返回纯文本
这时候就需要用不同的 Response 类。
2.2 JSONResponse —— 最常用的自定义 JSON 响应
当你需要自定义状态码、响应头,或者在异常处理器里返回错误信息时用:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/custom")
async def custom_response():
# 直接返回 dict → FastAPI 自动转 JSON(状态码 200)
return {"msg": "这是默认方式"}
@app.get("/custom-json")
async def custom_json_response():
# 用 JSONResponse → 可以自定义状态码和响应头
return JSONResponse(
status_code=200,
content={"msg": "这是 JSONResponse 方式"},
headers={"X-Custom-Header": "hello"} # 自定义响应头
)
JSONResponse vs 直接返回 dict:
| 方式 | 状态码 | 响应头 | 适用场景 |
|---|---|---|---|
return {...} | 固定 200 | 默认 | 普通接口 |
JSONResponse(...) | 自定义 | 自定义 | 错误处理、需要特殊响应头 |
2.3 HTMLResponse —— 返回 HTML 页面
有时候接口需要返回 HTML(比如登录页面、管理后台):
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/hello", response_class=HTMLResponse)
async def hello_page():
return """
<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<h1>Hello World!</h1>
<p>这是一个 HTML 页面</p>
</body>
</html>
"""
访问 /hello 时,浏览器会渲染成网页,而不是显示 JSON。
关键点:用 response_class=HTMLResponse 告诉 FastAPI 返回的是 HTML。
2.4 PlainTextResponse —— 返回纯文本
from fastapi.responses import PlainTextResponse
@app.get("/text")
async def plain_text():
return PlainTextResponse("这是纯文本,不是 JSON")
2.5 FileResponse —— 文件下载
当用户点击”下载”按钮时,后端需要返回一个文件:
from fastapi import FastAPI
from fastapi.responses import FileResponse
app = FastAPI()
@app.get("/download")
async def download_file():
# 返回本地文件,浏览器会自动触发下载
return FileResponse(
path="files/report.pdf", # 文件路径
filename="2024年报表.pdf", # 下载时显示的文件名
media_type="application/pdf" # 文件类型
)
常用 media_type:
| 文件类型 | media_type |
|---|---|
application/pdf | |
| Excel | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| Word | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
| 图片 | image/png、image/jpeg |
| 任意二进制 | application/octet-stream |
2.6 StreamingResponse —— 大文件流式下载
文件很大时(比如几百 MB),不能一次性读进内存,需要流式传输:
from fastapi.responses import StreamingResponse
@app.get("/stream")
async def stream_file():
def generate():
for i in range(10):
yield f"第 {i} 行数据\n" # yield 逐块返回
return StreamingResponse(
generate(), # 生成器
media_type="text/plain" # 内容类型
)
FileResponse vs StreamingResponse:
| FileResponse | StreamingResponse | |
|---|---|---|
| 适用场景 | 小文件,直接读取 | 大文件,逐块传输 |
| 内存占用 | 一次性读入内存 | 边读边发,省内存 |
| 用法 | 传文件路径 | 传生成器/迭代器 |
2.7 在接口中混合使用响应类型
同一个接口根据条件返回不同类型:
from fastapi import FastAPI, Query
from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
app = FastAPI()
@app.get("/report")
async def get_report(format: str = "json"):
data = {"sales": 1000, "profit": 200}
if format == "json":
return data # 默认 JSON
elif format == "html":
return HTMLResponse(f"""
<h1>报表</h1>
<p>销售额: {data['sales']}</p>
<p>利润: {data['profit']}</p>
""")
elif format == "file":
return FileResponse("reports/report.pdf", filename="报表.pdf")
else:
return JSONResponse(
status_code=400,
content={"error": f"不支持的格式: {format}"}
)
三、状态码(Status Code)
3.1 什么是状态码?
状态码是一个 3 位数字,放在 HTTP 响应里,告诉客户端”你的请求结果如何”。
三大类:
| 类别 | 含义 | 常见例子 |
|---|---|---|
| 2xx | ✅ 成功 | 200 OK, 201 Created, 204 No Content |
| 4xx | ❌ 你的请求有问题 | 400 Bad Request, 401 未登录, 403 没权限, 404 不存在, 422 数据校验失败 |
| 5xx | 💥 服务器内部出错了 | 500 Internal Server Error |
3.2 最常用的状态码详解
from fastapi import FastAPI, status
app = FastAPI()
# ============================================================
@app.get("/users/{user_id}", status_code=status.HTTP_200_OK)
async def get_user(user_id: int):
return {"id": user_id, "name": "张三"}
# ============================================================
@app.post("/users", status_code=status.HTTP_201_CREATED)
async def create_user():
return {"id": 1, "name": "张三", "msg": "用户创建成功"}
# ============================================================
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int):
# 假装删除成功了
return None # ← 必须返回 None,否则报错
# ============================================================
@app.post("/register", status_code=status.HTTP_201_CREATED)
async def register(username: str):
existing_users = ["admin", "root", "test"]
if username in existing_users:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail=f"用户名 {username} 已被占用")
return {"msg": "注册成功"}
# ============================================================
@app.get("/users/{user_id}")
async def get_user(user_id: int):
if user_id > 100: # 假设数据库里只有 1-100 号用户
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=f"用户 {user_id} 不存在")
return {"id": user_id, "name": "张三"}
3.3 两种设置状态码的方式
# 方式一:直接写数字(简单但不推荐)
@app.post("/items", status_code=201)
async def create_item():
return {"msg": "ok"}
# 好处:有代码提示,不会写错数字,可读性更好
@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item():
return {"msg": "ok"}
3.4 422 是怎么回事?
422 Unprocessable Entity 是 FastAPI 自动返回的,你不用手动设置。
当 Pydantic 校验失败时(比如类型错误、缺少必填字段、违反 Field 约束),FastAPI 自动返回 422:
class UserCreate(BaseModel):
username: str
age: int = Field(gt=0)
@app.post("/users")
async def create_user(user: UserCreate):
return {"msg": "ok"}
# FastAPI 自动返回 422,你不用写任何错误处理代码!
速查表
| 概念 | 一句话解释 | 关键代码 |
|---|---|---|
| response_model | 控制返回给客户端的字段 | @app.get(..., response_model=UserOut) |
| JSONResponse | 自定义 JSON 响应(状态码、响应头) | return JSONResponse(status_code=200, content={...}) |
| HTMLResponse | 返回 HTML 页面 | @app.get(..., response_class=HTMLResponse) |
| FileResponse | 文件下载 | return FileResponse(path="...", filename="...") |
| PlainTextResponse | 返回纯文本 | return PlainTextResponse("OK") |
| StreamingResponse | 大文件流式传输 | return StreamingResponse(generator) |
| status_code | 设置默认的成功状态码 | @app.post(..., status_code=201) |
速记卡(面试闪卡)
Q1:一句话讲清「响应模型与状态码」到底是什么?
A:FastAPI 用响应模型按字段白名单过滤返回数据(防泄密),用响应类型决定返回 JSON/HTML/文件等格式,用状态码告知请求成败——三者一起定制 HTTP 响应。
Q2:一、响应模型(Response Model) —— 怎么理解?
A:响应模型就像餐厅的”出餐盘”:你点的菜(请求)可能含密码这类厨房机密,但端上桌的盘子(响应)只放该给客人的东西。response_model=UserOut 会自动把密码等未定义字段过滤掉,避免把数据库敏感数据泄露给客户端。
Q3:二、响应类型(Response Types) —— 怎么理解?
A:响应类型好比同一个厨房能换不同”餐具”:默认出 JSON(盘),HTMLResponse 上网页(碗),FileResponse 出文件下载,StreamingResponse 边煮边端(大文件省内存不爆)。不同 Response 类对应不同 media_type,让一个接口既能返回网页也能下载文件或纯文本。
Q4:三、状态码(Status Code) —— 怎么理解?
A:状态码是 HTTP 响应的”红绿灯”:2xx 绿(成功,如 200/201/204),4xx 红(你的问题,如 400/401/404/422),5xx 崩(服务器炸了 500)。它用三位数字一句话告诉客户端”请求结果如何”,比读响应体更省事。
Q5:422 与状态码设置方式 —— 怎么理解?
A:设状态码有两种姿势:直接写数字(status_code=201)简单但易写错,或用 status 常量(status.HTTP_201_CREATED)有提示更稳。记住坑:Pydantic 校验失败是 422 不是 400——422 是 FastAPI 在你写代码前就自动抛的;400 才是你手动抛的业务错误(如用户名被占用)。
Q6:核心速记主线有哪些?
-
响应模型按字段白名单过滤,专治”返回密码”类泄密
-
响应类型 JSON/HTML/File/Streaming 各自管一种输出格式
-
状态码分 2xx/4xx/5xx,422 由 Pydantic 校验自动产生
-
设状态码优先用 status 常量,比裸数字更稳
-
请求模型与响应模型应分开定义(收密码 vs 露字段)
口诀
A:响应模型守清单,
密码外泄它来拦;
状态红绿报周全,
响应类型各专擅。
相关链接
-
目录:00-FastAPI
-
上一篇:03-请求体与Pydantic
-
下一篇:05-错误处理与数据校验