FastAPI Data Models
Introduction
Data models are the backbone of any FastAPI application. They provide a way to define the structure of data that your API will receive and return. In FastAPI, data models are built using Pydantic, a data validation and settings management library.
Using data models in your FastAPI applications offers several advantages:
- Data validation: Automatically validate incoming data against your model's requirements
- Type annotations: Leverage Python's type hints for better code documentation and editor support
- Automatic documentation: FastAPI uses your data models to generate OpenAPI documentation
- Data conversion: Convert incoming JSON data to Python objects and vice versa
In this tutorial, we'll explore how to create and use data models in FastAPI applications using Pydantic.
Basic Data Model Creation
Let's start by creating a simple data model for a user:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True # Default value
@app.post("/users/")
async def create_user(user: User):
return user
In this example:
- We create a
User
class that inherits fromBaseModel
- We define the fields with their types using type annotations
- The
is_active
field has a default value ofTrue
When you make a POST request to /users/
with JSON data, FastAPI will:
- Read the request body as JSON
- Convert the data to the types defined in the model
- Validate the data according to the model's fields
- Return a JSON response with the user data
For example, here's a valid request:
{
"id": 1,
"name": "John Doe",
"email": "[email protected]"
}
And the response would be:
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"is_active": true
}
Field Validation
Pydantic provides powerful validation features for your data models. Let's expand our User
model:
from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field
class User(BaseModel):
id: int = Field(..., gt=0, description="User ID must be positive")
name: str = Field(..., min_length=3, max_length=50)
email: EmailStr
is_active: bool = True
tags: Optional[List[str]] = None
In this example:
Field
provides validation constraints and metadata...
means the field is required (no default value)gt=0
ensures the ID is greater than zeromin_length
andmax_length
validate the string lengthEmailStr
validates that the string is a valid email (requires email-validator package)Optional[List[str]]
means the field can be null or a list of strings
To use email validation, you need to install the email-validator package:
pip install email-validator
Nested Models
You can create complex data structures by nesting models:
from typing import List
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class User(BaseModel):
id: int
name: str
email: str
addresses: List[Address]
@app.post("/users/")
async def create_user(user: User):
return user
Example request:
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"addresses": [
{
"street": "123 Main St",
"city": "New York",
"country": "USA",
"postal_code": "10001"
},
{
"street": "456 Park Ave",
"city": "Boston",
"country": "USA",
"postal_code": "02125"
}
]
}
FastAPI will validate the entire structure, including the nested Address
models.
Using Models for Request and Response
You can use different models for request and response:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import List, Optional
app = FastAPI()
# Request model
class UserCreate(BaseModel):
name: str
email: EmailStr
password: str
# Response model
class UserResponse(BaseModel):
id: int
name: str
email: EmailStr
is_active: bool
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
# Simulate creating a user in a database
new_user = {
"id": 1,
"name": user.name,
"email": user.email,
"is_active": True
}
return new_user
In this example:
UserCreate
is used to validate the request dataresponse_model=UserResponse
tells FastAPI to:- Filter the returned data to include only fields in the response model
- Validate the response against the model
- Document the response shape in the OpenAPI schema
When you make a request with:
{
"name": "John Doe",
"email": "[email protected]",
"password": "secretpassword"
}
The response will be:
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"is_active": true
}
Notice that the password
field is not included in the response.
Data Model Configuration
Pydantic models have a Config
inner class that allows you to customize behavior:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
password: str
class Config:
# Example configuration options
orm_mode = True # Allow conversion from ORM objects
schema_extra = {
"example": {
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"password": "secret"
}
}
Some common configurations:
orm_mode
: Allows the model to read data from objects with attributes (like ORM models)schema_extra
: Provides examples for documentationallow_population_by_field_name
: Allows populating by alias namesextra
: Controls behavior with extra fields ('ignore', 'allow', 'forbid')
Using Models with Database ORMs
One of the most powerful use cases for Pydantic models is integrating with database ORMs like SQLAlchemy:
from typing import List, Optional
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
# SQLAlchemy models (would be in a models.py file)
from sqlalchemy import Column, Integer, String, Boolean, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Setup database connection
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# SQLAlchemy model
class UserDB(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
# Create tables
Base.metadata.create_all(bind=engine)
# Pydantic models
class UserBase(BaseModel):
name: str
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
class Config:
orm_mode = True
app = FastAPI()
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
# Check if email already exists
db_user = db.query(UserDB).filter(UserDB.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
# Create new user (in real app, password would be hashed)
db_user = UserDB(
name=user.name,
email=user.email,
hashed_password=f"hashed_{user.password}"
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user # Works with orm_mode=True
@app.get("/users/", response_model=List[User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = db.query(UserDB).offset(skip).limit(limit).all()
return users
In this example:
- We define SQLAlchemy models for the database
- We define separate Pydantic models for API input/output
orm_mode = True
allows converting SQLAlchemy models to Pydantic models- We use dependency injection to manage database sessions
Advanced Pydantic Features
Pydantic offers many advanced features for sophisticated validation scenarios:
Custom validators
from pydantic import BaseModel, validator
class User(BaseModel):
id: int
name: str
password: str
password_confirm: str
@validator('password')
def password_strength(cls, v):
if len(v) < 8:
raise ValueError('Password must be at least 8 characters')
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one digit')
return v
@validator('password_confirm')
def passwords_match(cls, v, values, **kwargs):
if 'password' in values and v != values['password']:
raise ValueError('Passwords do not match')
return v
Field aliases
from pydantic import BaseModel, Field
class User(BaseModel):
user_id: int = Field(..., alias='id')
full_name: str = Field(..., alias='name')
This allows consuming JSON with different field names than your model.
Real-world Application Example
Let's build a simplified blog API using FastAPI and Pydantic models:
from fastapi import FastAPI, HTTPException, Depends, Query
from typing import List, Optional
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
from uuid import UUID, uuid4
app = FastAPI(title="Blog API")
# Database mock
db_users = {}
db_posts = {}
# Models
class UserBase(BaseModel):
email: EmailStr
name: str
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class User(UserBase):
id: UUID
created_at: datetime
class Config:
orm_mode = True
class PostBase(BaseModel):
title: str = Field(..., min_length=3, max_length=100)
content: str = Field(..., min_length=10)
published: bool = True
class PostCreate(PostBase):
pass
class Post(PostBase):
id: UUID
author_id: UUID
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
class PostWithAuthor(Post):
author: User
# Endpoints
@app.post("/users/", response_model=User)
def create_user(user: UserCreate):
if user.email in [u.email for u in db_users.values()]:
raise HTTPException(status_code=400, detail="Email already registered")
user_id = uuid4()
db_user = {
**user.dict(),
"id": user_id,
"created_at": datetime.now()
}
db_users[user_id] = db_user
return db_user
@app.get("/users/", response_model=List[User])
def read_users(skip: int = 0, limit: int = 100):
return list(db_users.values())[skip: skip + limit]
@app.post("/posts/", response_model=Post)
def create_post(post: PostCreate, author_id: UUID):
if author_id not in db_users:
raise HTTPException(status_code=404, detail="User not found")
post_id = uuid4()
db_post = {
**post.dict(),
"id": post_id,
"author_id": author_id,
"created_at": datetime.now(),
"updated_at": None
}
db_posts[post_id] = db_post
return db_post
@app.get("/posts/", response_model=List[PostWithAuthor])
def read_posts(
skip: int = 0,
limit: int = 100,
published: Optional[bool] = Query(None, description="Filter by published status")
):
filtered_posts = db_posts.values()
if published is not None:
filtered_posts = [p for p in filtered_posts if p["published"] == published]
posts_with_authors = []
for post in list(filtered_posts)[skip: skip + limit]:
# Add author information to each post
post_with_author = {
**post,
"author": db_users[post["author_id"]]
}
posts_with_authors.append(post_with_author)
return posts_with_authors
This example shows:
- Models with inheritance to share common fields
- Using UUID for identifiers
- Datetime fields for timestamps
- Different models for request and response
- Nested response models (
PostWithAuthor
) - Query parameters with validation and documentation
Summary
FastAPI data models powered by Pydantic provide a robust foundation for building APIs:
- They enforce data validation at runtime
- Convert between JSON data and Python objects
- Generate API documentation automatically
- Support complex nested structures and relationships
- Enable clean separation between database and API layers
- Provide rich validation tools for data integrity
By effectively using data models, you can build safer, more maintainable APIs with less code.
Additional Resources
- FastAPI Official Documentation on Data Models
- Pydantic Documentation
- SQLAlchemy + FastAPI Integration
Exercises
- Create a Product data model with validation for price (must be positive) and inventory count
- Build a simple TODO API with models for tasks, including validation for due dates
- Extend the blog example to include comments on posts with appropriate models
- Create a model using Pydantic's
root_validator
to implement conditional validation between fields
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)