Skip to main content

FastAPI Model Serialization

Introduction

When building APIs with FastAPI, one of the most crucial aspects is transforming data between different formats - particularly from Python objects to JSON for API responses, and from JSON to Python objects for incoming requests. This process is known as serialization (Python → JSON) and deserialization (JSON → Python).

FastAPI uses Pydantic models at its core to handle this data conversion automatically, making it much easier to validate, serialize, and document your API data. In this tutorial, we'll explore how FastAPI handles model serialization using Pydantic and how you can customize this process to suit your application needs.

Understanding Model Serialization in FastAPI

What is Serialization?

Serialization is the process of converting complex data structures (like Python objects) into formats that can be easily stored or transmitted (like JSON strings). In the context of FastAPI:

  • Serialization: Converting Python objects to JSON to send as API responses
  • Deserialization: Converting incoming JSON data to Python objects based on your models

FastAPI handles both processes automatically using Pydantic, which provides type checking and data validation along the way.

Basic Model Serialization

Let's start with a simple example of how FastAPI uses Pydantic for serialization:

python
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
is_available: bool = True
tags: List[str] = []

@app.post("/items/")
async def create_item(item: Item):
return item

In this example:

  1. We define a Pydantic model Item
  2. When a POST request is made to /items/, FastAPI automatically:
    • Deserializes the JSON request body into an Item instance
    • Validates the data against the model's types and constraints
    • Returns the item, which FastAPI automatically serializes back to JSON

Example Request and Response

Let's see what happens when we make a request to this endpoint:

Request:

json
{
"name": "Laptop",
"price": 999.99,
"description": "A powerful development machine",
"tags": ["electronics", "computers"]
}

Response:

json
{
"name": "Laptop",
"description": "A powerful development machine",
"price": 999.99,
"is_available": true,
"tags": ["electronics", "computers"]
}

FastAPI automatically handled deserializing the JSON input into a Pydantic Item object, then serialized the object back to JSON in the response, including default values for fields not provided in the request.

Customizing Serialization

Model Config and Field Customization

Pydantic provides several ways to customize how your models serialize. One common approach is through the model's Config class:

python
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI()

class Product(BaseModel):
id: int
name: str
price: float = Field(..., gt=0)
description: Optional[str] = None

class Config:
# Exclude unset fields from serialized output
exclude_unset = True
# Schema example for documentation
schema_extra = {
"example": {
"id": 123,
"name": "Premium Headphones",
"price": 129.99,
"description": "Noise-cancelling wireless headphones"
}
}

@app.post("/products/")
async def create_product(product: Product):
return product

With exclude_unset=True, fields not included in the input won't appear in the output when the model is serialized.

Field Aliases

Sometimes the JSON field names in your API need to differ from your Python attribute names. Field aliases solve this problem:

python
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class User(BaseModel):
user_id: int = Field(..., alias="userId")
full_name: str = Field(..., alias="fullName")
email_address: str = Field(..., alias="email")

class Config:
# Allow population by alias
allow_population_by_field_name = True

@app.post("/users/")
async def create_user(user: User):
return user

With this setup, you can:

  • Accept JSON with camelCase fields (userId, fullName, email)
  • Work with snake_case attributes in your Python code (user_id, full_name, email_address)
  • Return camelCase in your API responses

Example Request and Response with Aliases

Request:

json
{
"userId": 1001,
"fullName": "Jane Smith",
"email": "[email protected]"
}

Response:

json
{
"userId": 1001,
"fullName": "Jane Smith",
"email": "[email protected]"
}

Custom Serialization Methods

For more advanced serialization control, Pydantic models support custom serialization methods:

Using model_dump()

The model_dump() method (which replaces the deprecated dict() method in newer Pydantic versions) converts a Pydantic model to a dictionary:

python
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Dict, Any
from datetime import datetime

app = FastAPI()

class Order(BaseModel):
id: int
items: list[str]
created_at: datetime = datetime.now()

@app.post("/orders/")
async def create_order(order: Order):
# Custom processing on the model
order_dict = order.model_dump()
# Add calculated fields
order_dict["item_count"] = len(order.items)
order_dict["processed_at"] = datetime.now().isoformat()
return order_dict

Using model_dump_json()

For direct JSON serialization:

python
from fastapi import FastAPI, Response
from pydantic import BaseModel
from typing import List
from datetime import date

app = FastAPI()

class Event(BaseModel):
name: str
date: date
attendees: List[str]

@app.post("/events/")
async def create_event(event: Event):
# Custom JSON with specific formatting
json_data = event.model_dump_json(indent=2)
return Response(content=json_data, media_type="application/json")

Handling Complex Data Types

Date and DateTime Serialization

FastAPI automatically handles common data types like datetime, date, and UUID:

python
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime, date
import uuid

app = FastAPI()

class Appointment(BaseModel):
id: uuid.UUID = uuid.uuid4()
title: str
date: date
time: datetime
duration_minutes: int

@app.post("/appointments/")
async def create_appointment(appointment: Appointment):
return appointment

Request:

json
{
"title": "Dental Check-up",
"date": "2023-04-15",
"time": "2023-04-15T14:30:00",
"duration_minutes": 30
}

Response:

json
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Dental Check-up",
"date": "2023-04-15",
"time": "2023-04-15T14:30:00",
"duration_minutes": 30
}

Custom Type Encoders

For types not automatically handled by Pydantic, you can define custom encoders:

python
from fastapi import FastAPI
from pydantic import BaseModel, field_serializer
from datetime import timedelta

app = FastAPI()

class TrainingSession(BaseModel):
name: str
duration: timedelta

# Custom serializer for timedelta
@field_serializer('duration')
def serialize_duration(self, duration: timedelta):
# Convert to minutes for the API
return round(duration.total_seconds() / 60)

@app.post("/training/")
async def create_training(session: TrainingSession):
return session

Request:

json
{
"name": "Interval Training",
"duration": "00:45:00"
}

Response:

json
{
"name": "Interval Training",
"duration": 45
}

Recursive Models and Relationships

FastAPI handles recursive data structures and relationships between models:

python
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Comment(BaseModel):
id: int
text: str
replies: List['Comment'] = []

# This is needed for recursive Pydantic models
Comment.model_rebuild()

class Post(BaseModel):
id: int
title: str
content: str
comments: List[Comment] = []

@app.post("/posts/")
async def create_post(post: Post):
return post

Schema Customization for Documentation

FastAPI uses your Pydantic models to generate OpenAPI documentation. You can customize this:

python
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI()

class Product(BaseModel):
name: str = Field(
...,
title="Product Name",
description="The name of the product",
max_length=50
)
price: float = Field(
...,
gt=0,
description="The price must be greater than zero"
)
description: Optional[str] = Field(
None,
title="Product Description",
description="Optional detailed description of the product"
)

@app.post(
"/products/",
response_description="The created product",
summary="Create a new product",
description="Create a new product with the provided details."
)
async def create_product(
product: Product = Body(
...,
examples={
"basic": {
"summary": "A basic example",
"value": {
"name": "Smartphone",
"price": 699.99
}
},
"complete": {
"summary": "A complete example",
"description": "A complete product with all fields",
"value": {
"name": "Premium Smartphone",
"price": 999.99,
"description": "The latest flagship smartphone with advanced features"
}
}
}
)
):
return product

Real-world Application Example: E-commerce API

Let's build a more complete example of an e-commerce product API with proper serialization:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, validator, field_serializer
from typing import List, Optional, Dict
from datetime import datetime
from enum import Enum
import uuid

app = FastAPI()

# Database simulation
products_db = {}

class ProductCategory(str, Enum):
ELECTRONICS = "electronics"
CLOTHING = "clothing"
BOOKS = "books"
HOME = "home"
OTHER = "other"

class ProductImage(BaseModel):
url: str
alt_text: Optional[str] = None

class ProductVariant(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
sku: str
price: float
stock_quantity: int

@validator('price')
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Price must be positive')
return v

class Product(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
description: str
base_price: float
category: ProductCategory
tags: List[str] = []
images: List[ProductImage] = []
variants: List[ProductVariant] = []
created_at: datetime = Field(default_factory=datetime.now)
updated_at: Optional[datetime] = None

@field_serializer('created_at', 'updated_at')
def serialize_datetime(self, dt: Optional[datetime]):
if dt:
return dt.isoformat()
return None

class Config:
schema_extra = {
"example": {
"name": "Comfortable Cotton T-Shirt",
"description": "A premium cotton t-shirt that's perfect for everyday wear",
"base_price": 19.99,
"category": "clothing",
"tags": ["cotton", "t-shirt", "casual"],
"images": [
{"url": "https://example.com/tshirt1.jpg", "alt_text": "Front view"}
],
"variants": [
{
"name": "Small Black",
"sku": "TS-BLK-S",
"price": 19.99,
"stock_quantity": 25
}
]
}
}

class ProductResponse(BaseModel):
product: Product
available_variants: int
total_stock: int
lowest_price: float
highest_price: float

@app.post("/products/", response_model=ProductResponse)
async def create_product(product: Product):
# Simulate saving to database
products_db[product.id] = product

# Calculate derived fields for response
available_variants = len(product.variants)
total_stock = sum(variant.stock_quantity for variant in product.variants) if product.variants else 0

if product.variants:
prices = [variant.price for variant in product.variants]
lowest_price = min(prices)
highest_price = max(prices)
else:
lowest_price = highest_price = product.base_price

# Return enriched response
return ProductResponse(
product=product,
available_variants=available_variants,
total_stock=total_stock,
lowest_price=lowest_price,
highest_price=highest_price
)

@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: str):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")

product = products_db[product_id]

# Calculate same derived fields
available_variants = len(product.variants)
total_stock = sum(variant.stock_quantity for variant in product.variants) if product.variants else 0

if product.variants:
prices = [variant.price for variant in product.variants]
lowest_price = min(prices)
highest_price = max(prices)
else:
lowest_price = highest_price = product.base_price

return ProductResponse(
product=product,
available_variants=available_variants,
total_stock=total_stock,
lowest_price=lowest_price,
highest_price=highest_price
)

This e-commerce example demonstrates:

  1. Complex nested Pydantic models with relationships (Product containing ProductVariant and ProductImage)
  2. Custom field validation with validator
  3. Custom serialization with field_serializer
  4. Enum usage for categorical data
  5. Response model customization with derived fields
  6. Schema documentation with examples

Summary

FastAPI's model serialization, powered by Pydantic, allows you to:

  • Automatically convert between JSON and Python objects
  • Validate incoming data against type definitions and constraints
  • Customize field names, aliases, and serialization formats
  • Handle complex data types and relationships
  • Generate comprehensive API documentation

By leveraging Pydantic models for serialization, you can create robust APIs with less code and fewer bugs, while maintaining complete control over how your data is transformed and validated.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Serialization: Create a FastAPI application with a User model that includes fields for name, email, age, and registration date. Implement endpoints to create and retrieve users.

  2. Custom Serialization: Extend the user model to include a password field that should never be included in API responses. Implement proper serialization to exclude sensitive data.

  3. Complex Relationships: Build a blog API with models for Post, Author, and Comment with proper relationships between them. Implement endpoints that return nested data.

  4. Transformation Challenge: Create a model that accepts dates in multiple formats (e.g., "YYYY-MM-DD", "MM/DD/YYYY") and always outputs them in ISO format.

  5. API Response Customization: Implement an API endpoint that can return user data in different formats based on a query parameter (e.g., ?format=detailed vs ?format=summary).



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)