FastAPI: 响应模型 — 精准控制 API 输出

最后更新:2026-08-26

响应模型就像舞台灯光——只照亮该让观众看到的区域,后台设备(内部字段)留在暗处,既美观又安全。

1. 你将学到


2. Alice 的真实故事

(1) 痛点:成本价泄露给竞争对手

Alice 的 PriceTracker 存储了商品的零售价和批发价。某天 Bob 前端请求商品详情时,API 把批发价也返回了,竞争对手抓取 API 后获得了所有批发价信息,Alice 的客户非常不满。问题根源是 Flask 没有响应过滤机制,ORM 对象所有字段直接序列化返回。

(2) response_model 的解法

FastAPI 的 response_model 声明式过滤——只返回模型中声明的字段,未声明的字段自动剔除,从架构层面杜绝数据泄露。

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class ProductPublic(BaseModel):
    id: int
    name: str
    retail_price: float  # Only retail price

class ProductAdmin(BaseModel):
    id: int
    name: str
    retail_price: float
    wholesale_price: float  # Includes wholesale

app = FastAPI()

@app.get("/products/{id}", response_model=ProductPublic)
async def get_product_public(id: int):
    # Even if DB has wholesale_price, response_model filters it out
    return {"id": id, "name": "Widget", "retail_price": 29.99, "wholesale_price": 15.0}

(3) 收益

响应过滤从"记得手动删除字段"变成"声明模型自动过滤",批发价泄露问题彻底消失。公开 API 只返回 ProductPublic 声明的字段,管理 API 用 ProductAdmin 返回完整数据。


3. response_model 基础

(1) 自动过滤原理

100%
flowchart LR
    A[ORM Object] --> B{response_model}
    B -->|Declared fields| C[JSON Response]
    B -->|Undeclared fields| D[Filtered Out]
    
    subgraph ProductPublic
        E[id]
        F[name]
        G[retail_price]
    end
    
    subgraph Hidden
        H[wholesale_price]
        I[cost_price]
        J[supplier_id]
    end

▶ 示例:response_model 自动过滤

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class ProductResponse(BaseModel):
    id: int
    name: str
    category: str

# Simulated database record with extra fields
DB_PRODUCT = {
    "id": 1,
    "name": "Widget",
    "category": "electronics",
    "cost_price": 8.50,  # Should NOT be in response
    "supplier_id": 42,   # Should NOT be in response
}

app = FastAPI()

@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: int):
    # cost_price and supplier_id are auto-filtered
    return DB_PRODUCT

输出:

TEXT 📖 仅展示
{"id": 1, "name": "Widget", "category": "electronics"}

(2) response_model 与返回类型对比

方式 过滤行为 文档生成 推荐场景
response_model=X 自动过滤 始终推荐
返回类型 -> X 不过滤 仅类型标注
无声明 不过滤 不推荐

▶ 示例:response_model vs 返回类型

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class ProductBrief(BaseModel):
    id: int
    name: str

app = FastAPI()

@app.get("/demo/model", response_model=ProductBrief)
async def with_response_model():
    # response_model FILTERS: only id and name in response
    return {"id": 1, "name": "Widget", "secret": "hidden"}

@app.get("/demo/type") -> ProductBrief
async def with_return_type():
    # Return type does NOT filter: secret field leaks!
    return {"id": 1, "name": "Widget", "secret": "leaked"}

输出:

TEXT 📖 仅展示
# 函数定义成功

4. 字段级精细过滤

(1) exclude 与 include

当不想为每种场景都创建新模型时,可用 response_model_excluderesponse_model_include 动态过滤字段。

▶ 示例:exclude 排除敏感字段

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel, Field

class ProductFull(BaseModel):
    id: int
    name: str
    retail_price: float
    wholesale_price: float = Field(exclude=True)  # Default exclude
    cost_price: float = Field(exclude=True)
    supplier: str = Field(exclude=True)

app = FastAPI()

@app.get("/products/{id}", response_model=ProductFull)
async def get_product(id: int):
    return {
        "id": id,
        "name": "Widget",
        "retail_price": 29.99,
        "wholesale_price": 15.0,
        "cost_price": 8.50,
        "supplier": "Acme Corp",
    }

输出:

TEXT 📖 仅展示
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

输出(wholesale_price、cost_price、supplier 被过滤):

TEXT 📖 仅展示
{"id": 1, "name": "Widget", "retail_price": 29.99}

▶ 示例:response_model_exclude 动态排除

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class ProductFull(BaseModel):
    id: int
    name: str
    retail_price: float
    wholesale_price: float
    cost_price: float

app = FastAPI()

@app.get(
    "/products/{id}",
    response_model=ProductFull,
    response_model_exclude={"wholesale_price", "cost_price"},
)
async def get_product_public(id: int):
    return {
        "id": id, "name": "Widget",
        "retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
    }

@app.get(
    "/admin/products/{id}",
    response_model=ProductFull,
)
async def get_product_admin(id: int):
    return {
        "id": id, "name": "Widget",
        "retail_price": 29.99, "wholesale_price": 15.0, "cost_price": 8.50,
    }

输出:

TEXT 📖 仅展示
# 函数定义成功
过滤方式 适用场景 粒度
response_model=ModelA 不同角色看不同模型 模型级
response_model_exclude={fields} 排除少量字段 字段级
response_model_include={fields} 只包含少量字段 字段级
Field(exclude=True) 固定排除某个字段 字段定义级

5. 多响应模型与高级技巧

(1) 同一端点不同响应

▶ 示例:Union 响应模型

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union

class ProductFound(BaseModel):
    id: int
    name: str
    price: float

class ProductNotFound(BaseModel):
    error: str
    product_id: int

app = FastAPI()

@app.get("/products/{id}", response_model=Union[ProductFound, ProductNotFound])
async def search_product(id: int):
    if id == 1:
        return ProductFound(id=1, name="Widget", price=9.99)
    return ProductNotFound(error="Not found", product_id=id)

输出:

TEXT 📖 仅展示
# 函数定义成功

▶ 示例:列表响应模型

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel

class PriceEntry(BaseModel):
    product_id: int
    price: float
    recorded_at: str

app = FastAPI()

@app.get("/products/{id}/prices", response_model=list[PriceEntry])
async def get_price_history(id: int):
    return [
        {"product_id": id, "price": 29.99, "recorded_at": "2026-01-01"},
        {"product_id": id, "price": 24.99, "recorded_at": "2026-02-01"},
        {"product_id": id, "price": 34.99, "recorded_at": "2026-03-01"},
    ]

输出:

TEXT 📖 仅展示
# 函数定义成功

(2) exclude_unset 与 exclude_none

▶ 示例:exclude_unset 只返回有值的字段

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

class ProductUpdate(BaseModel):
    name: Optional[str] = None
    category: Optional[str] = None
    price: Optional[float] = None

app = FastAPI()

@app.get(
    "/products/{id}",
    response_model=ProductUpdate,
    response_model_exclude_unset=True,
)
async def get_product_partial(id: int):
    # Only name was set, category and price remain None
    return ProductUpdate(name="Updated Widget")

输出:

TEXT 📖 仅展示
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

输出(只包含设置了值的字段):

TEXT 📖 仅展示
{"name": "Updated Widget"}
选项 效果 适用场景
response_model_exclude_unset=True 排除未设置的字段 PATCH 响应,只返回更新的字段
response_model_exclude_none=True 排除值为 None 的字段 精简响应,去除空值
response_model_exclude_defaults=True 排除使用默认值的字段 去除冗余默认值
response_model_by_alias=True 用别名输出字段 前端需要驼峰命名

▶ 示例:response_model_by_alias

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel, Field, ConfigDict

class ProductResponse(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    id: int
    product_name: str = Field(alias="productName")
    unit_price: float = Field(alias="unitPrice", description="Price in USD")

app = FastAPI()

@app.get("/products/{id}", response_model=ProductResponse, response_model_by_alias=True)
async def get_product(id: int):
    return {"id": id, "productName": "Widget", "unitPrice": 9.99}

输出:

TEXT 📖 仅展示
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

输出(字段名使用 alias):

TEXT 📖 仅展示
{"id": 1, "productName": "Widget", "unitPrice": 9.99}

❓ 常见问题

Q response_model 和返回类型注解有什么区别?
A response_model 会过滤未声明字段并校验输出,返回类型注解仅影响 OpenAPI 文档生成,不会过滤字段。始终用 response_model。
Q exclude 和 include 能同时用吗?
A 不能同时使用。选其一即可,exclude 排除指定字段,include 只包含指定字段。
Q exclude_unset 在什么场景最有用?
A PATCH 请求的响应——客户端只更新了部分字段,响应也只返回被更新的字段,避免混淆。
Q 嵌套模型的字段能被 exclude 吗?
A 可以,用点号路径 {"nested_model": {"secret_field"}} 排除嵌套字段。
Q 列表响应怎么声明?
Aresponse_model=list[ItemModel],FastAPI 会自动将列表中每个元素按模型过滤。
Q response_model 会影响性能吗?
A 有轻微开销(序列化+过滤),但换来的是数据安全和文档一致性。百万级 QPS 场景可用 orjson 优化序列化。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建 ProductPublic 模型(id, name, price),用 response_model 让端点只返回这三个字段,即使返回 dict 含有 cost_price 也不泄露。提示:@app.get(..., response_model=ProductPublic)
  2. 进阶题(难度⭐⭐):为 PriceTracker 创建两个端点——公开端点隐藏 wholesale_price,管理端点显示完整字段。用 response_model_exclude 实现。提示:response_model_exclude={"wholesale_price"}
  3. 挑战题(难度⭐⭐⭐):创建 ProductDetail 模型含可选字段(description, image_url 等),用 response_model_exclude_unset=True 让端点只返回客户端提供的字段,空值不出现在 JSON 中。提示:Optional[str] = None + response_model_exclude_unset=True

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏