响应模型与状态码

写给初学者:每个概念都从”为什么需要它”开始讲,配合通俗比喻和完整可运行代码。


一、响应模型(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
PDFapplication/pdf
Excelapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Wordapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
图片image/pngimage/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:

FileResponseStreamingResponse
适用场景小文件,直接读取大文件,逐块传输
内存占用一次性读入内存边读边发,省内存
用法传文件路径传生成器/迭代器

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:响应模型守清单,

密码外泄它来拦;

状态红绿报周全,

响应类型各专擅。

相关链接


技术学习路线图 > 进阶篇