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_userEven 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:
response_model_exclude_unset: When set toTrue, fields with default values that were not explicitly assigned on the returned object are excluded from the output.response_model_exclude_defaults: Removes all fields that match their defined default values.response_model_exclude_none: Drops fields that evaluate toNone.response_model_exclude: Accepts a set of field names as strings to exclude explicitly.response_model_include: Accepts a set of field names as strings, returning only the specified attributes.
@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.