How to serialize responses¶
Flask-Jeroboam validates and serializes your responses using Pydantic models. This ensures your API returns data in the expected format and catches bugs before they reach clients.
Simple response models¶
Define what your endpoint returns using Pydantic models:
from pydantic import BaseModel
class ItemOut(BaseModel):
id: int
name: str
price: float
@app.get("/items/<int:item_id>", response_model=ItemOut)
def get_item(item_id: int):
item = fetch_item_from_db(item_id)
return item
Jeroboam validates that your return value matches ItemOut. If a field is missing, an error is raised in development.
Lists and collections¶
Return lists by wrapping the model in a list type:
from typing import List
@app.get("/items", response_model=List[ItemOut])
def list_items():
items = fetch_all_items()
return items
Field aliases¶
Serialize with different names than your Python fields:
from pydantic import BaseModel, Field
class ItemOut(BaseModel):
id: int
item_name: str = Field(..., alias="name")
price_usd: float = Field(..., alias="price")
@app.get("/items/<int:item_id>", response_model=ItemOut)
def get_item(item_id: int):
return fetch_item(item_id)
Response JSON will be {"name": "...", "price": ...} even though your Python code uses different names.
Excluding fields¶
Sometimes you fetch more data than you want to return:
class ItemOut(BaseModel):
id: int
name: str
price: float
class Config:
# Only these fields are included in responses
fields = {"id", "name", "price"}
@app.get("/items/<int:item_id>", response_model=ItemOut)
def get_item(item_id: int):
# Internal model might have secret_key, password, etc.
item = fetch_item_internal(item_id)
return item # Only id, name, price are returned
Nested responses¶
Use nested models for complex structures:
from typing import List
class AuthorOut(BaseModel):
id: int
name: str
class BookOut(BaseModel):
id: int
title: str
author: AuthorOut
@app.get("/books/<int:book_id>", response_model=BookOut)
def get_book(book_id: int):
book = fetch_book_with_author(book_id)
return book
The response will include the nested author object, automatically serialized.
Optional responses¶
Endpoints can return None sometimes:
from typing import Optional
@app.get("/items/<int:item_id>", response_model=Optional[ItemOut])
def get_item_if_exists(item_id: int):
item = fetch_item(item_id)
return item # Could be None
Returning None will serialize to null in JSON.
Type coercion¶
Pydantic coerces values to match the model:
class ItemOut(BaseModel):
id: int
quantity: int
@app.get("/items/<int:item_id>", response_model=ItemOut)
def get_item(item_id: int):
# fetch_item returns quantities as strings "42"
item = fetch_item(item_id)
return item # Pydantic coerces "42" to 42
Your code returns strings but the response contains integers.
Computed fields¶
Add fields that are calculated on the fly:
from pydantic import computed_field
class ItemOut(BaseModel):
name: str
quantity: int
price_each: float
@computed_field
@property
def total_value(self) -> float:
return self.quantity * self.price_each
@app.get("/items/<int:item_id>", response_model=ItemOut)
def get_item(item_id: int):
item = fetch_item(item_id)
return item
The response includes total_value even though it’s not in the database. It’s computed from quantity and price_each.
Custom serializers¶
Transform data during serialization:
from datetime import datetime
from pydantic import BaseModel, field_serializer
class EventOut(BaseModel):
name: str
created_at: datetime
@field_serializer('created_at')
def serialize_datetime(self, value: datetime):
return value.isoformat()
@app.get("/events/<int:event_id>", response_model=EventOut)
def get_event(event_id: int):
event = fetch_event(event_id)
return event
Datetimes are serialized as ISO strings instead of the default format.