← Python Code FastAPI
Browse Python Concepts

Pydantic Models — Response Model Polymorphism

Mental Model

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.

Rule: Never use base classes as response models when returning dynamic subclasses; define specific Union types or separate endpoints instead.

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?

Broken code
Python
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
    )
Predict what JSON payload the client receives when querying the /user endpoint.

The Output

What actually happens
{ "id": 123, "username": "alice" }

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

Corrected pattern
Python
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

Never use base classes as response models when returning dynamic subclasses; define specific Union types or separate endpoints instead.
Common mistake: Expecting response_model to automatically handle polymorphism by serializing all fields of a returned subclass, even if the declared response model is a base class.