Response Model — Precise Control of API Output
A response model is like stage lighting—it illuminates only the areas the audience is meant to see, while keeping the backstage equipment (internal fields) in the dark, ensuring both aesthetics and security.
1. What You'll Learn
response_modelBasics: Automatic Filtering of Undeclared Fields, Serialization Controlresponse_model_exclude/response_model_include: Fine-grained filtering at the field level- Multi-response model: A single endpoint returns different models (
Union/list response) response_model_by_aliasandresponse_model_exclude_unset: Practical Tips- Alice Scenario: PriceTracker Price Response—Public Version (Hidden Cost Price) vs. Admin Version (Full Price Chain)
2. Alice's True Story
(1) Pain Point: Cost prices being leaked to competitors
Alice's PriceTracker stores the retail and wholesale prices of products. One day, when Bob requested product details via the front end, the API returned the wholesale prices as well. A competitor scraped the API and obtained all the wholesale price information, causing Alice's customers to become very dissatisfied. The root cause of the problem was that Flask lacks a response filtering mechanism, and all fields of the ORM object were serialized and returned directly.
(2) Solution to response_model
FastAPI's response_model declarative filtering—returns only the fields declared in the model, automatically excludes undeclared fields, and prevents data leaks at the schema level.
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) Revenue
Response filtering has changed from "Remember to manually delete fields" to "Automatic filtering based on declared models," completely eliminating the issue of wholesale price leaks. The public API now returns only the fields declared in ProductPublic, while the management API returns the complete data using ProductAdmin.
3. response_model Basics
(1) Principles of Automatic Filtering
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
(1) ▶ Example: Automatic filtering with response_model
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
Output:
{"id": 1, "name": "Widget", "category": "electronics"}
(2) Comparison of response_model and Return Types
| Method | Filtering Behavior | Document Generation | Recommended Scenarios |
|---|---|---|---|
response_model=X |
Auto-filter | Yes | Always recommend |
Return Type -> X |
No Filtering | Yes | Type Annotation Only |
| No statement | No filtering | None | Not recommended |
(2) ▶ Example: response_model vs. return type
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"}
Output:
# Function defined successfully
4. Fine-Grained Filtering at the Field Level
(1) exclude and include
When you don't want to create a new model for every scenario, you can use response_model_exclude and response_model_include to dynamically filter fields.
(1) ▶ Example: exclude to exclude sensitive fields
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",
}
Output:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Output (wholesale_price, cost_price, and supplier have been filtered out):
{"id": 1, "name": "Widget", "retail_price": 29.99}
(2) ▶ Example: response_model_exclude Dynamic Exclusion
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,
}
Output:
# Function defined successfully
| Filtering Method | Applicable Scenarios | Granularity |
|---|---|---|
response_model=ModelA |
Different Models from Different Perspectives | Model Level |
response_model_exclude={fields} |
Exclude a small number of fields | Field level |
response_model_include={fields} |
Contains only a few fields | Field-level |
Field(exclude=True) |
Exclude a specific field | Field definition level |
5. Multi-response Models and Advanced Techniques
(1) Different Responses for the Same Endpoint
(1) ▶ Example: Union Response Model
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)
Output:
# Function defined successfully
(2) ▶ Example: List Response Model
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"},
]
Output:
# Function defined successfully
(2) exclude_unset and exclude_none
(3) ▶ Example: exclude_unset returns only fields with values
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")
Output:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Output (includes only fields with values):
{"name": "Updated Widget"}
| Option | Effect | Use Cases |
|---|---|---|
response_model_exclude_unset=True |
Exclude fields that have not been set | PATCH response: returns only the updated fields |
response_model_exclude_none=True |
Exclude fields with a value of None | Streamline the response by removing null values |
response_model_exclude_defaults=True |
Exclude fields that use default values | Remove redundant default values |
response_model_by_alias=True |
Output fields using aliases | The front end requires camelCase naming |
(4) ▶ Example: response_model_by_alias
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}
Output:
INFO: 127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK
Output (using aliases for field names):
{"id": 1, "productName": "Widget", "unitPrice": 9.99}
❓ FAQ
response_model and the return type annotation?response_model filters out undeclared fields and validates the output, while the return type annotation only affects OpenAPI documentation generation and does not filter fields. Always use response_model.exclude_unset most useful?{"nested_model": {"secret_field"}}.response_model=list[ItemModel], and FastAPI will automatically filter each element in the list according to the model.response_model affect performance?orjson to optimize serialization.📖 Summary
response_modelAutomatically filters out undeclared fields to prevent sensitive data leaks at the schema levelresponse_model_exclude/response_model_includeprovides fine-grained control at the field level without the need to create a new modelUnionThe response model supports returning different structures from the same endpoint; seelist[Model]for list responses.response_model_exclude_unsetReturns only fields with assigned values; suitable for PATCH scenariosresponse_model_by_alias=TrueUse aliases for responses to meet front-end camelCase naming conventions
📝 Exercises
- Basic Question (Difficulty ⭐): Create a
ProductPublicmodel (id, name, price), and useresponse_modelto ensure the endpoint returns only these three fields—even if it returns a dict containingcost_price, no information is leaked. Hint:@app.get(..., response_model=ProductPublic) - Advanced Exercise (Difficulty ⭐⭐): Create two endpoints for PriceTracker—a public endpoint that hides
wholesale_price, and an admin endpoint that displays all fields. Implement this usingresponse_model_exclude. Hint:response_model_exclude={"wholesale_price"} - Challenge (Difficulty ⭐⭐⭐): Create a
ProductDetailmodel with optional fields (such as description, image_url, etc.), and useresponse_model_exclude_unset=Trueto ensure that the endpoint returns only the fields provided by the client, with empty values omitted from the JSON. Hint:Optional[str] = None+response_model_exclude_unset=True
---|



