Pydantic Models — Response Model Polymorphism
Think of response_model as a strict contract or a schema filter applied to the outgoing data. It defines the exact structure and fields that are allowed to be present in the response, regardless of what the underlying Python object actually contains.
The Setup
You have a base user model and a premium subclass containing subscription details. To support dynamic outputs while retaining code structure, you configure the route's response_model to be the base class but return the premium class instance.
What Does This Print?
from fastapi import FastAPI
from pydantic import BaseModel
class BaseUser(BaseModel):
id: int
username: str
class PremiumUser(BaseUser):
premium_token: str
discount_rate: float
app = FastAPI()
@app.get("/user", response_model=BaseUser)
async def get_user():
# We return a PremiumUser instance
return PremiumUser(
id=123,
username="alice",
premium_token="gold-99",
discount_rate=0.15
)
/user endpoint.
The Output
The server returns a 200 OK status code, but the JSON payload contains only the fields declared in BaseUser:
The fields premium_token and discount_rate are silently stripped out of the response payload without raising any validation exceptions.
Why Python Does This
FastAPI is designed to validate, filter, and serialize outgoing data based on the type defined in the response_model parameter. Under the hood, FastAPI creates a serialization execution plan using the properties of the model defined in response_model (here, BaseUser). Even though your handler returned a valid instance of PremiumUser, FastAPI converts the return value into a dictionary and feeds it directly into BaseUser.model_validate() or serializes it using the BaseUser serialization metadata. This ensures that field exclusion rules and structural contracts are strictly maintained, but it silently discards subclass data.
The Fix
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
class BaseUser(BaseModel):
id: int
username: str
class PremiumUser(BaseUser):
premium_token: str
discount_rate: float
app = FastAPI()
# Union types allow FastAPI to select the correct matching schema during serialization
@app.get("/user", response_model=Union[PremiumUser, BaseUser])
async def get_user() -> Union[PremiumUser, BaseUser]:
return PremiumUser(
id=123,
username="alice",
premium_token="gold-99",
discount_rate=0.15
)
By defining response_model as a Union of BaseUser and PremiumUser, or by using response_model_include to explicitly list the fields, you provide FastAPI with a more precise schema. This allows all relevant fields from the actual object to be included in the response, adhering to a defined output contract.
How This Fails in Real Systems
An e-commerce API returned polymorphic payment method details. Due to the route using the base payment schema as its response model, API clients were billed correctly but could not retrieve transaction hashes on the confirmation page, resulting in hundreds of manual customer support tickets.
Key Takeaway
response_model to automatically handle polymorphism by serializing all fields of a returned subclass, even if the declared response model is a base class.