Pydantic Models

Difficulty: Intermediate

Pydantic is the data validation library at the heart of FastAPI. It uses Python type annotations to validate data, serialize and deserialize objects, and generate JSON Schema definitions. Every request body in FastAPI is defined using a Pydantic model, and understanding Pydantic is essential for building robust APIs.

A Pydantic model is a Python class that inherits from BaseModel. You define fields as class attributes with type annotations, and Pydantic automatically validates any data you pass to the constructor. If the data does not match the type annotations, Pydantic raises a ValidationError with detailed information about what went wrong.

When FastAPI receives a request with a JSON body, it automatically parses the JSON and passes it to your Pydantic model. If validation fails, FastAPI returns a 422 response with the validation errors. If validation succeeds, your handler receives a fully validated, typed Pydantic model instance.

Pydantic models support all Python types: str, int, float, bool, list, dict, Optional, Union, and custom types. They also support complex types like datetime, UUID, Decimal, URL, email addresses, and more. Pydantic converts compatible types automatically - for example, a string '42' will be converted to the integer 42 if the field type is int.

The model_dump() method (called dict() in Pydantic v1) converts a model instance to a dictionary. model_dump_json() converts it to a JSON string. The model_json_schema() class method returns the JSON Schema for the model. FastAPI uses these internally for serialization and documentation generation.

Pydantic v2 (used with modern FastAPI) is significantly faster than v1 because it uses a Rust core for validation. It also introduced ConfigDict for model configuration, the Annotated type for field metadata, and better support for custom validators.

Code examples

Basic Pydantic Model

from pydantic import BaseModel
from datetime import datetime

class User(BaseModel):
    name: str
    email: str
    age: int
    is_active: bool = True
    created_at: datetime | None = None

# Valid data
user = User(name="Alice", email="alice@example.com", age=30)
print(user.name)           # "Alice"
print(user.is_active)      # True (default)
print(user.model_dump())   # {"name": "Alice", "email": ...}

# Type coercion
user2 = User(name="Bob", email="bob@example.com", age="25")
print(user2.age)           # 25 (string converted to int)
print(type(user2.age))     # <class 'int'>

# Invalid data raises ValidationError
try:
    User(name="Charlie", email="charlie@example.com", age="not a number")
except Exception as e:
    print(e)  # Validation error: age must be an integer

Pydantic models validate data on creation. Fields with defaults are optional. Type coercion converts compatible types (string '25' to int 25). Invalid data raises ValidationError with detailed error messages.

Using Pydantic with FastAPI

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

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

@app.post("/items")
def create_item(item: ItemCreate):
    total = item.price + item.tax
    return {
        item.model_dump(),
        "total_price": total,
    }

# POST /items
# Body: {"name": "Widget", "price": 9.99}
# Response: {"name": "Widget", "description": null, "price": 9.99, "tax": 0.0, "total_price": 9.99}

# POST /items
# Body: {"name": "Widget"}  (missing required 'price')
# Response: 422 Validation Error

FastAPI automatically parses the JSON request body and validates it against the Pydantic model. The handler receives a validated ItemCreate instance. model_dump() converts the model to a dict for spreading into the response.

Model Methods and Serialization

from pydantic import BaseModel

class Product(BaseModel):
    id: int
    name: str
    price: float
    tags: list[str] = []

product = Product(id=1, name="Laptop", price=999.99, tags=["electronics", "computers"])

# Convert to dictionary
data = product.model_dump()
# {"id": 1, "name": "Laptop", "price": 999.99, "tags": ["electronics", "computers"]}

# Exclude specific fields
data_no_id = product.model_dump(exclude={"id"})
# {"name": "Laptop", "price": 999.99, "tags": ["electronics", "computers"]}

# Include only specific fields
data_minimal = product.model_dump(include={"name", "price"})
# {"name": "Laptop", "price": 999.99}

# Convert to JSON string
json_str = product.model_dump_json()
# '{"id":1,"name":"Laptop","price":999.99,"tags":["electronics","computers"]}'

# Create from dictionary
product2 = Product.model_validate({"id": 2, "name": "Mouse", "price": 29.99})

# Get JSON Schema
schema = Product.model_json_schema()
# {"properties": {"id": {"type": "integer"}, ...}, "required": ["id", "name", "price"]}

Pydantic models have rich serialization methods. model_dump() converts to dict with optional field filtering. model_dump_json() converts to JSON string. model_validate() creates a model from a dict. model_json_schema() generates the JSON Schema.

Key points

Concepts covered

BaseModel, Type Validation, Serialization, model_dump, JSON Schema