Skip to main content

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:

python
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 from BaseModel
  • We define the fields with their types using type annotations
  • The is_active field has a default value of True

When you make a POST request to /users/ with JSON data, FastAPI will:

  1. Read the request body as JSON
  2. Convert the data to the types defined in the model
  3. Validate the data according to the model's fields
  4. Return a JSON response with the user data

For example, here's a valid request:

json
{
"id": 1,
"name": "John Doe",
"email": "[email protected]"
}

And the response would be:

json
{
"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:

python
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 zero
  • min_length and max_length validate the string length
  • EmailStr 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:

bash
pip install email-validator

Nested Models

You can create complex data structures by nesting models:

python
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:

json
{
"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:

python
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 data
  • response_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:

json
{
"name": "John Doe",
"email": "[email protected]",
"password": "secretpassword"
}

The response will be:

json
{
"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:

python
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 documentation
  • allow_population_by_field_name: Allows populating by alias names
  • extra: 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:

python
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

python
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

python
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:

python
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

Exercises

  1. Create a Product data model with validation for price (must be positive) and inventory count
  2. Build a simple TODO API with models for tasks, including validation for due dates
  3. Extend the blog example to include comments on posts with appropriate models
  4. 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! :)