Filtering Sensitive Data with FastAPI Response Models

FastAPI secures and structures API outputs by leveraging Pydantic schemas through the response_model parameter in route decorators. This mechanism automatically intercepts outgoing data, validates it against the target schema, strips undeclared attributes such as plain-text secrets or hashed passwords, and formats the response to ensure sensitive internal application states never leak to the client.

Declaring Output Schemas

In FastAPI, the data returned by a route handler function does not have to match the schema exposed to the client. By providing a dedicated output schema to the response_model argument, FastAPI filters out any dictionary keys or object attributes that are not explicitly defined in that schema.

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()

# Input schema
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

# Database schema representation
class UserInDB(BaseModel):
    id: int
    username: str
    email: EmailStr
    hashed_password: str

# Public output schema
class UserPublic(BaseModel):
    id: int
    username: str
    email: EmailStr

@app.post("/users/", response_model=UserPublic)
def create_user(user: UserCreate):
    # Simulated database record containing a hashed password
    db_user = UserInDB(
        id=1,
        username=user.username,
        email=user.email,
        hashed_password="supersecret_hash_value"
    )
    # The return object contains 'hashed_password', but FastAPI strips it
    return db_user

Even though the create_user function returns an object containing hashed_password, FastAPI serializes the response using UserPublic. The client only receives id, username, and email.

ORM and Database Model Compatibility

When using ORMs like SQLAlchemy, Peewee, or Tortoise, database objects typically contain private database keys or relations that should not be public. Pydantic allows response models to read attributes directly from ORM instances rather than requiring dictionary mappings.

In Pydantic v2, this is configured using from_attributes:

from pydantic import BaseModel, ConfigDict

class UserPublic(BaseModel):
    id: int
    username: str
    
    model_config = ConfigDict(from_attributes=True)

With from_attributes=True enabled, FastAPI can accept raw ORM instances directly from database queries. It extracts only the fields declared in UserPublic and ignores the rest.

Fine-Grained Filtering with Route Parameters

FastAPI provides route-level parameters to dynamically control serialization without creating multiple distinct schemas for every scenario:

@app.get("/items/{item_id}", response_model=ItemSchema, response_model_exclude_none=True)
def get_item(item_id: str):
    return {"name": "Widget", "internal_notes": None}

In this case, internal_notes is stripped from the JSON response because its value is None.

Return Type Annotations vs. response_model

FastAPI supports Python standard type annotations for output filtering. If a function is annotated with a return type, FastAPI treats it as the default response model:

@app.get("/profile/")
def get_profile() -> UserPublic:
    return fetch_user_data()

However, explicitly declaring response_model=UserPublic in the decorator takes precedence. This explicit definition is required when the internal return type (such as an ORM instance or internal dictionary containing raw secrets) differs from the public schema type.