FastAPI Model Config
When building APIs with FastAPI, Pydantic models are a cornerstone for data validation and serialization. The Config
class nested within these models allows you to fine-tune how your models behave, affecting validation, serialization, and schema generation. This guide explains the Config
class and how to customize it for your needs.
Introduction to Model Config
Every Pydantic model in FastAPI can include a nested Config
class that controls the model's behavior. This powerful feature lets you customize validation rules, JSON schema settings, ORM integration, and more.
Here's a simple example of a Pydantic model with a Config
class:
from pydantic import BaseModel
class User(BaseModel):
name: str
email: str
password: str
class Config:
# Configuration settings go here
extra = "forbid" # Disallow extra fields
validate_assignment = True # Validate data when assigning attributes after creation
Common Config Options
Let's explore the most commonly used configuration options:
Extra Fields Behavior
The extra
attribute controls how a model handles fields that aren't defined in the model:
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class UserStrict(BaseModel):
name: str
class Config:
extra = "forbid" # Raise error for extra fields
class UserFlexible(BaseModel):
name: str
class Config:
extra = "ignore" # Ignore extra fields
class UserCollect(BaseModel):
name: str
class Config:
extra = "allow" # Allow and include extra fields
@app.post("/strict-user/")
def create_strict_user(user: UserStrict):
return user
@app.post("/flexible-user/")
def create_flexible_user(user: UserFlexible):
return user
@app.post("/collect-user/")
def create_collect_user(user: UserCollect):
return user
Input for /strict-user/
:
{
"name": "John",
"age": 30
}
Output:
{
"detail": [
{
"loc": ["body", "age"],
"msg": "extra fields not permitted",
"type": "value_error.extra"
}
]
}
Input for /flexible-user/
:
{
"name": "John",
"age": 30
}
Output:
{
"name": "John"
}
Input for /collect-user/
:
{
"name": "John",
"age": 30
}
Output:
{
"name": "John",
"age": 30
}
Schema Customization
Customize how your model appears in the JSON Schema and OpenAPI documentation:
from pydantic import BaseModel, Field
from fastapi import FastAPI
app = FastAPI()
class UserProfile(BaseModel):
name: str
bio: str = Field(default="")
class Config:
title = "User Profile Schema"
description = "A model representing a user's public profile"
schema_extra = {
"example": {
"name": "Jane Doe",
"bio": "Full-stack developer with 5 years of experience"
}
}
@app.post("/users/")
def create_user(user: UserProfile):
return user
This configuration enhances your API documentation with clearer titles, descriptions, and example values.
Validation Behavior
Control how and when validation occurs:
from pydantic import BaseModel, validator
from fastapi import FastAPI
app = FastAPI()
class Product(BaseModel):
name: str
price: float
@validator('price')
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Price must be positive')
return v
class Config:
validate_assignment = True # Validate when attributes are assigned
validate_all = True # Run validation even on fields not explicitly set
@app.get("/product-example")
def get_product():
# This will be validated at assignment time due to validate_assignment=True
product = Product(name="Laptop", price=999.99)
try:
# This will fail validation
product.price = -10
except ValueError as e:
return {"error": str(e)}
return product
Output:
{
"error": "Price must be positive"
}
ORM Mode
When working with databases, ORM mode allows Pydantic to work more efficiently with ORMs like SQLAlchemy:
from typing import List, Optional
from fastapi import FastAPI, Depends
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session, sessionmaker
from pydantic import BaseModel
app = FastAPI()
# SQLAlchemy setup
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)
# Pydantic models
class UserBase(BaseModel):
name: str
email: str
class UserCreate(UserBase):
pass
class User(UserBase):
id: int
class Config:
orm_mode = True # Enable ORM mode
# Database dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Create tables
Base.metadata.create_all(bind=engine)
@app.post("/users/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = UserDB(name=user.name, email=user.email)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user # ORM mode automatically converts this to User model
The orm_mode = True
setting allows Pydantic to access attributes from the ORM model rather than assuming a dictionary structure, making it seamless to work with database models.
Additional Configuration Options
Here are some other useful config options:
Alias Generation
Control how field names are represented in JSON:
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class UserModel(BaseModel):
user_id: int
first_name: str
last_name: str
class Config:
alias_generator = lambda field_name: field_name.upper()
allow_population_by_field_name = True
@app.post("/users/")
def create_user(user: UserModel):
return {
"model_fields": {
"user_id": user.user_id,
"first_name": user.first_name,
"last_name": user.last_name
},
"json_fields": user.dict(by_alias=True)
}
Input:
{
"USER_ID": 123,
"FIRST_NAME": "John",
"LAST_NAME": "Doe"
}
Output:
{
"model_fields": {
"user_id": 123,
"first_name": "John",
"last_name": "Doe"
},
"json_fields": {
"USER_ID": 123,
"FIRST_NAME": "John",
"LAST_NAME": "Doe"
}
}
Custom Types and JSON Encoding
Configure how custom data types are encoded to JSON:
import datetime
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class Event(BaseModel):
title: str
timestamp: datetime.datetime
class Config:
json_encoders = {
datetime.datetime: lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S")
}
@app.get("/current-event")
def get_current_event():
return Event(
title="Conference",
timestamp=datetime.datetime.now()
)
Output:
{
"title": "Conference",
"timestamp": "2023-07-15 14:30:22"
}
Real-World Application: Advanced API Configuration
Let's build a more complete example showing how Config can be used in a real-world API:
from typing import List, Optional, Dict, Any
from datetime import datetime
from pydantic import BaseModel, Field, validator
from fastapi import FastAPI, HTTPException
app = FastAPI()
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class Config:
validate_assignment = True
schema_extra = {
"example": {
"street": "123 Main St",
"city": "New York",
"country": "USA",
"postal_code": "10001"
}
}
class User(BaseModel):
id: Optional[int] = None
username: str
email: str
full_name: str
disabled: bool = False
address: Optional[Address] = None
created_at: datetime = Field(default_factory=datetime.now)
@validator('email')
def email_must_contain_at(cls, v):
if '@' not in v:
raise ValueError('email must contain @')
return v
class Config:
title = "User Model"
extra = "forbid"
validate_assignment = True
allow_mutation = True # Whether the model is mutable after creation
json_encoders = {
datetime: lambda dt: dt.isoformat()
}
schema_extra = {
"example": {
"username": "johndoe",
"email": "[email protected]",
"full_name": "John Doe",
"disabled": False
}
}
# Mock database
db: Dict[int, User] = {}
next_id = 1
@app.post("/users/", response_model=User)
def create_user(user: User):
global next_id
user.id = next_id
db[next_id] = user
next_id += 1
return user
@app.get("/users/{user_id}", response_model=User)
def get_user(user_id: int):
if user_id not in db:
raise HTTPException(status_code=404, detail="User not found")
return db[user_id]
@app.get("/users/", response_model=List[User])
def list_users():
return list(db.values())
@app.put("/users/{user_id}", response_model=User)
def update_user(user_id: int, updated_user: User):
if user_id not in db:
raise HTTPException(status_code=404, detail="User not found")
# Keep original ID and created_at
updated_user.id = user_id
updated_user.created_at = db[user_id].created_at
db[user_id] = updated_user
return updated_user
This example demonstrates:
- Nested models with their own configurations
- Custom validators with model behavior control
- JSON encoding for datetime objects
- Schema examples to enhance documentation
- Immutability control through
allow_mutation
Summary
The Pydantic Config
class provides powerful customization options for your FastAPI models. The main configuration options include:
- Extra fields handling:
extra = "forbid"
,"ignore"
, or"allow"
- Validation behavior:
validate_assignment
,validate_all
- ORM integration:
orm_mode = True
- Schema customization:
title
,description
,schema_extra
- JSON serialization:
json_encoders
- Field naming:
alias_generator
,allow_population_by_field_name
By mastering these configuration options, you can fine-tune how your models behave, improving API reliability, documentation quality, and development experience.
Additional Resources
- Pydantic Configuration Documentation
- FastAPI Documentation on Models
- Model Schema Customization in FastAPI
Exercises
- Create a model for a blog post with
Config
settings that enforce strict validation and provide example data in the documentation. - Implement a Pydantic model with custom field aliases to handle data from an external API that uses snake_case while your API uses camelCase.
- Build a user registration model with custom validators and appropriate Config settings to handle sensitive data correctly.
- Create a model for a database entity with ORM mode enabled, demonstrating integration with SQLAlchemy.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)