404 Not Found

404 Not Found


nginx

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


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.

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) 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

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

(1) ▶ Example: Automatic filtering with 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

Output:

TEXT
{"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

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"}

Output:

TEXT
# 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

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",
    }

Output:

TEXT
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):

TEXT
{"id": 1, "name": "Widget", "retail_price": 29.99}

(2) ▶ Example: response_model_exclude Dynamic Exclusion

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,
    }

Output:

TEXT
# 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

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)

Output:

TEXT
# Function defined successfully

(2) ▶ Example: List Response Model

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"},
    ]

Output:

TEXT
# Function defined successfully

(2) exclude_unset and exclude_none

(3) ▶ Example: exclude_unset returns only fields with values

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")

Output:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

Output (includes only fields with values):

TEXT
{"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

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}

Output:

TEXT
INFO:     127.0.0.1:50123 - "GET /api/items HTTP/1.1" 200 OK

Output (using aliases for field names):

TEXT
{"id": 1, "productName": "Widget", "unitPrice": 9.99}

❓ FAQ

Q What is the difference between response_model and the return type annotation?
A 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.
Q Can "exclude" and "include" be used at the same time?
A No, they cannot be used at the same time. Choose one or the other: "exclude" excludes the specified fields, while "include" includes only the specified fields.
Q In what scenarios is exclude_unset most useful?
A In responses to PATCH requests—when the client updates only some fields, the response returns only the updated fields to avoid confusion.
Q Can fields in a nested model be excluded?
A Yes, you can exclude nested fields using a dot-separated path {"nested_model": {"secret_field"}}.
Q How do I declare a list response?
A Use response_model=list[ItemModel], and FastAPI will automatically filter each element in the list according to the model.
Q Does response_model affect performance?
A There is a slight overhead (serialization + filtering), but this ensures data security and documentation consistency. In scenarios with millions of QPS, you can use orjson to optimize serialization.

📖 Summary


📝 Exercises

  1. Basic Question (Difficulty ⭐): Create a ProductPublic model (id, name, price), and use response_model to ensure the endpoint returns only these three fields—even if it returns a dict containing cost_price, no information is leaked. Hint: @app.get(..., response_model=ProductPublic)
  2. 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 using response_model_exclude. Hint: response_model_exclude={"wholesale_price"}
  3. Challenge (Difficulty ⭐⭐⭐): Create a ProductDetail model with optional fields (such as description, image_url, etc.), and use response_model_exclude_unset=True to 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

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

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

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