Skip to main content

FastAPI Pydantic Introduction

What is Pydantic?

Pydantic is a data validation and settings management library using Python type annotations. It's one of the core components that makes FastAPI powerful, efficient, and developer-friendly. When working with FastAPI, Pydantic allows you to define how data should be in pure, standard Python; it will then validate your data and convert it to the appropriate type.

Why Use Pydantic with FastAPI?

Before diving into how Pydantic works, let's understand why it's such an essential part of FastAPI:

  1. Type Safety: Helps catch errors during development instead of at runtime.
  2. Automatic Validation: Validates incoming request data against your defined schemas.
  3. Documentation: Automatically generates API documentation from your models.
  4. Serialization/Deserialization: Converts between Python objects and JSON seamlessly.

Installing Pydantic

Pydantic is installed automatically when you install FastAPI, but you can also install it separately:

bash
pip install pydantic

Basic Pydantic Models

Let's start with a simple example to understand how Pydantic models work:

python
from pydantic import BaseModel

class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True # Default value
tags: list[str] = [] # Default empty list

In this example:

  • User is a Pydantic model that inherits from BaseModel
  • We define fields with their types using Python's type annotations
  • We can provide default values for fields

Creating an Instance

python
# Create a user with data
user = User(id=1, name="John Doe", email="[email protected]")
print(user)

Output:

id=1 name='John Doe' email='[email protected]' is_active=True tags=[]

Data Validation

Pydantic will automatically validate the data and raise clear errors when invalid:

python
try:
# This will fail because id should be an integer
invalid_user = User(id="not_an_integer", name="Jane Doe", email="[email protected]")
except Exception as e:
print(f"Validation error: {e}")

Output:

Validation error: 1 validation error for User
id
value is not a valid integer (type=type_error.integer)

Working with JSON

Pydantic seamlessly handles conversion between Python objects and JSON:

python
user = User(id=1, name="John Doe", email="[email protected]", tags=["admin", "user"])

# Convert to a dictionary
user_dict = user.model_dump()
print(user_dict)

# Convert to JSON string
user_json = user.model_dump_json()
print(user_json)

Output:

{'id': 1, 'name': 'John Doe', 'email': '[email protected]', 'is_active': True, 'tags': ['admin', 'user']}
{"id":1,"name":"John Doe","email":"[email protected]","is_active":true,"tags":["admin","user"]}

Parsing JSON

python
json_data = '{"id": 2, "name": "Jane Doe", "email": "[email protected]", "tags": ["editor"]}'
user = User.model_validate_json(json_data)
print(user)

Output:

id=2 name='Jane Doe' email='[email protected]' is_active=True tags=['editor']

Advanced Validation

Pydantic offers advanced validation features like:

Field Constraints

python
from pydantic import BaseModel, Field, EmailStr

class AdvancedUser(BaseModel):
id: int = Field(..., gt=0) # Required field, must be greater than 0
name: str = Field(..., min_length=2, max_length=50) # Length constraints
email: EmailStr # Special email validation type
age: int = Field(None, ge=18, description="Age must be 18 or greater")

Note: You'll need to install the email validator package:

bash
pip install "pydantic[email]"

Custom Validators

You can define custom validation logic using validators:

python
from pydantic import BaseModel, validator

class Product(BaseModel):
name: str
price: float
discount: float = 0.0

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

@validator('discount')
def discount_must_be_valid(cls, value, values):
if value < 0:
raise ValueError('Discount cannot be negative')
if 'price' in values and value > values['price']:
raise ValueError('Discount cannot be greater than price')
return value

Using Pydantic with FastAPI

Now let's see how Pydantic integrates with FastAPI:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None

@app.post("/items/")
def create_item(item: Item):
# FastAPI automatically validates the request body against the Item model
# If validation fails, it returns a 422 Unprocessable Entity error

total_price = item.price
if item.tax:
total_price += item.tax

return {
"item": item,
"total_price": total_price
}

When you create an API endpoint with FastAPI:

  1. FastAPI will read the request body as JSON
  2. Convert the JSON to a Python object using Pydantic
  3. Validate the data
  4. Create an instance of your model
  5. Pass it to your function

If any validation errors occur, FastAPI will automatically return a detailed error message to the client.

Real-World Example: User Registration API

Let's build a more complete example of a user registration API using Pydantic and FastAPI:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, EmailStr, validator
from typing import Optional
import re

app = FastAPI()

# Define our Pydantic models
class UserRegistration(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: EmailStr
password: str = Field(..., min_length=8)
full_name: Optional[str] = None

# Custom validator for username format
@validator('username')
def username_alphanumeric(cls, v):
if not re.match(r'^[a-zA-Z0-9_]+$', v):
raise ValueError('Username must be alphanumeric')
return v

# Custom validator for password strength
@validator('password')
def password_strength(cls, v):
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one number')
if not any(char.isupper() for char in v):
raise ValueError('Password must contain at least one uppercase letter')
return v

class UserResponse(BaseModel):
username: str
email: str
full_name: Optional[str] = None

# Mock database
users_db = {}

@app.post("/register/", response_model=UserResponse)
def register_user(user: UserRegistration):
# Check if username exists
if user.username in users_db:
raise HTTPException(status_code=400, detail="Username already registered")

# In a real application, you would hash the password here

# Save user to our mock database
users_db[user.username] = user.model_dump()

# Return the user without the password
return UserResponse(
username=user.username,
email=user.email,
full_name=user.full_name
)

In this example:

  1. We use Pydantic to define a schema for user registration with validations
  2. We create a separate schema for the response (without the password)
  3. FastAPI uses these schemas to validate input and output data
  4. Our custom validators ensure data quality (username format, password strength)

Common Pydantic Features

Optional Fields and Default Values

python
from typing import Optional
from pydantic import BaseModel

class Product(BaseModel):
name: str # Required field
description: Optional[str] = None # Optional field with default None
price: float # Required field
available: bool = True # Optional field with default True

Nested Models

python
from pydantic import BaseModel
from typing import List

class Address(BaseModel):
street: str
city: str
postal_code: str

class User(BaseModel):
name: str
addresses: List[Address] # A list of Address objects

Type Aliases

python
from pydantic import BaseModel, Field
from typing import List

# Define a type alias for readability
Tags = List[str]

class Item(BaseModel):
name: str
tags: Tags = Field(default_factory=list)

Summary

Pydantic is a powerful data validation library that works seamlessly with FastAPI to:

  • Define data schemas using standard Python type hints
  • Validate incoming data automatically
  • Convert between JSON and Python objects
  • Generate OpenAPI documentation
  • Provide clear error messages for invalid data

By leveraging Pydantic with FastAPI, you can build robust, type-safe, and well-documented APIs with less code and fewer bugs.

Additional Resources

Practice Exercises

  1. Create a Pydantic model for a blog post with fields like title, content, author, publication date, and tags.
  2. Add validation to ensure the title is between 5 and 100 characters.
  3. Create a FastAPI endpoint to create a new blog post using your model.
  4. Add custom validation to check that the publication date is not in the future.
  5. Create a separate model for the response that includes an auto-generated ID field.


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