Skip to main content

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:

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

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

json
{
"username": "johndoe",
"email": "[email protected]"
}

Output Example:

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

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

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

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

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

python
@app.post("/customers/")
async def create_customer(customer: Customer):
return customer

Input Example:

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

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

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

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

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

json
{
"id": 1,
"firstName": "John",
"lastName": "Doe"
}

Access in Python:

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:

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

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

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

  1. 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.

  2. Inheritance Exercise: Create a base Person model and extend it to create Employee and Customer models with appropriate additional fields.

  3. 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.

  4. 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.

  5. 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! :)