FastAPI Model Manipulation
Introduction
Pydantic models are the backbone of FastAPI's data handling system. They provide validation, serialization, and documentation capabilities that make your API development process smoother and more reliable. In this tutorial, we'll explore how to manipulate Pydantic models within FastAPI applications, allowing you to transform, validate, and work with data effectively.
Model manipulation helps you:
- Transform input data to match your requirements
- Validate data against business rules
- Convert between different data formats
- Create complex data workflows
Let's dive into the practical aspects of FastAPI model manipulation!
Basic Model Operations
Creating and Validating Models
The foundation of model manipulation is creating Pydantic models that define your data structure:
from pydantic import BaseModel, Field, validator
from typing import Optional, List
import datetime
class User(BaseModel):
id: Optional[int] = None
username: str = Field(..., min_length=3, max_length=50)
email: str
created_at: datetime.datetime = datetime.datetime.now()
# Basic validator
@validator('email')
def email_must_contain_at(cls, v):
if '@' not in v:
raise ValueError('email must contain @')
return v.lower() # Normalize email by converting to lowercase
When you use this model in FastAPI, validation happens automatically:
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.post("/users/")
async def create_user(user: User):
# Data is already validated here
print(f"Creating user with email: {user.email}")
return user
Input Example:
{
"username": "johndoe",
"email": "[email protected]"
}
Output Example:
{
"id": null,
"username": "johndoe",
"email": "[email protected]",
"created_at": "2023-07-14T12:34:56.789012"
}
Notice that the email was automatically converted to lowercase!
Advanced Model Transformations
Using Root Validators
Root validators allow you to validate or transform multiple fields at once:
from pydantic import BaseModel, root_validator
class SignupRequest(BaseModel):
username: str
password: str
password_confirm: str
@root_validator
def check_passwords_match(cls, values):
pw1, pw2 = values.get('password'), values.get('password_confirm')
if pw1 != pw2:
raise ValueError('passwords do not match')
return values
Pre and Post Processing with Config
Pydantic's config options allow you to customize validation behavior:
class UserProfile(BaseModel):
name: str
bio: Optional[str] = None
class Config:
anystr_strip_whitespace = True # Strips whitespace from strings
validate_assignment = True # Validates during attribute assignment
extra = "ignore" # Ignores extra fields
Model Inheritance and Composition
Model Inheritance
You can extend models through inheritance:
class BaseUser(BaseModel):
username: str
email: str
class AdminUser(BaseUser):
is_admin: bool = True
permissions: List[str] = []
Model Composition
Compose models by embedding one model inside another:
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class Customer(BaseModel):
name: str
shipping_address: Address
billing_address: Optional[Address] = None
Example usage:
@app.post("/customers/")
async def create_customer(customer: Customer):
return customer
Input Example:
{
"name": "Jane Smith",
"shipping_address": {
"street": "123 Main St",
"city": "Boston",
"country": "USA",
"postal_code": "02108"
}
}
Practical Model Manipulation Techniques
Excluding and Including Fields
Control which fields are included in the output:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
class Item(BaseModel):
name: str
description: str
price: float
tax: float = 0.0
tags: List[str] = []
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
# Create response excluding tax
return item.dict(exclude={"tax"})
@app.get("/items/full/")
async def get_items_with_tax():
item = Item(name="Foo", description="Bar", price=20.0, tax=1.5)
# Only include specific fields
return item.dict(include={"name", "price", "tax"})
Converting Between Models
Transform one model into another:
class UserIn(BaseModel):
username: str
password: str
email: str
full_name: Optional[str] = None
class UserOut(BaseModel):
username: str
email: str
full_name: Optional[str] = None
@app.post("/users/", response_model=UserOut)
async def create_user(user: UserIn):
# Notice that password won't be included in the response
return user
Using Update Methods
Update models with new data:
@app.patch("/users/{user_id}")
async def update_user(user_id: int, user_update: UserUpdate):
# Fetch existing user from database
current_user = get_user_from_db(user_id)
# Update only the fields that are provided
updated_user_data = current_user.copy(update=user_update.dict(exclude_unset=True))
# Save to database
save_user_to_db(updated_user_data)
return updated_user_data
Working with Aliases and Custom Fields
Field Aliases
Use aliases to map between different field names:
from pydantic import BaseModel, Field
class StudentRecord(BaseModel):
student_id: int = Field(..., alias="id")
first_name: str = Field(..., alias="firstName")
last_name: str = Field(..., alias="lastName")
class Config:
allow_population_by_field_name = True
This allows you to receive camelCase JSON but work with snake_case in Python:
Input Example:
{
"id": 1,
"firstName": "John",
"lastName": "Doe"
}
Access in Python:
# Both work!
student.first_name
student.firstName
Real-World Example: Form Processing API
Let's create a more complex example of a form processing API that shows various model manipulation techniques:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, validator, root_validator
from typing import Optional, List
from enum import Enum
import datetime
app = FastAPI()
class FormStatus(str, Enum):
DRAFT = "draft"
SUBMITTED = "submitted"
APPROVED = "approved"
REJECTED = "rejected"
class FormField(BaseModel):
name: str
value: str
@validator('name')
def name_must_be_valid(cls, v):
allowed_fields = {"name", "email", "phone", "comments"}
if v not in allowed_fields:
raise ValueError(f"Field {v} is not allowed")
return v
@validator('value')
def validate_field_value(cls, v, values):
if 'name' in values and values['name'] == 'email':
if '@' not in v:
raise ValueError("Invalid email format")
return v
class FormSubmission(BaseModel):
id: Optional[int] = None
fields: List[FormField]
status: FormStatus = FormStatus.DRAFT
submission_date: Optional[datetime.datetime] = None
@root_validator
def check_required_fields(cls, values):
fields = values.get('fields', [])
field_names = {field.name for field in fields}
if 'name' not in field_names or 'email' not in field_names:
raise ValueError("Name and email fields are required")
# Set submission date if status is SUBMITTED
if values.get('status') == FormStatus.SUBMITTED and not values.get('submission_date'):
values['submission_date'] = datetime.datetime.now()
return values
def anonymize(self):
"""Remove sensitive information for logging purposes"""
fields = [f for f in self.fields if f.name != 'email']
return FormSubmission(
id=self.id,
fields=fields,
status=self.status,
submission_date=self.submission_date
)
@app.post("/forms/", response_model=FormSubmission)
async def submit_form(form: FormSubmission):
# Log the anonymized form data
anonymized = form.anonymize()
print(f"Received form: {anonymized.dict()}")
# Set ID (in a real app, this would be from the database)
form.id = 12345
# Process based on status
if form.status == FormStatus.SUBMITTED:
# Do submission processing
pass
return form
Input Example:
{
"fields": [
{"name": "name", "value": "Jane Smith"},
{"name": "email", "value": "[email protected]"},
{"name": "phone", "value": "555-1234"},
{"name": "comments", "value": "Please contact me ASAP"}
],
"status": "submitted"
}
Output Example:
{
"id": 12345,
"fields": [
{"name": "name", "value": "Jane Smith"},
{"name": "email", "value": "[email protected]"},
{"name": "phone", "value": "555-1234"},
{"name": "comments", "value": "Please contact me ASAP"}
],
"status": "submitted",
"submission_date": "2023-07-14T12:34:56.789012"
}
Summary
In this tutorial, we covered a wide range of techniques for manipulating Pydantic models in FastAPI:
- Basic model creation and validation
- Advanced validators including field and root validators
- Model inheritance and composition
- Field exclusion and inclusion
- Model conversion and updates
- Working with aliases and custom fields
- A comprehensive real-world example
These techniques allow you to build robust APIs that properly validate, transform, and process data according to your application's needs.
Additional Resources and Exercises
Resources
Exercises
-
Basic Model Exercise: Create a
Product
model with fields for name, price, and inventory. Add validators to ensure price is positive and inventory is non-negative. -
Inheritance Exercise: Create a base
Person
model and extend it to createEmployee
andCustomer
models with appropriate additional fields. -
Advanced Transformation: Build a model for a blog post that automatically generates a URL slug from the title and ensures all links in the content are valid URLs.
-
Complex Validation: Create a model for a shopping cart that validates that item quantities don't exceed available inventory and calculates a total with tax.
-
Model Conversion: Design a system that converts between different API versions of the same model, adding or removing fields as needed.
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)