Advanced Body and Validation

Loading concept...

FastAPI Request Data Handling - Advanced Body and Validation

The Story: Building a Smart Pizza Order System

Imagine you’re building a magic pizza ordering machine. Customers tell the machine what they want, and it needs to understand everything perfectly—the pizza type, toppings, delivery address, and special instructions. If something’s wrong (like ordering -5 pizzas), the machine should politely say “Oops, that doesn’t work!”

That’s exactly what FastAPI’s advanced body handling and validation does for your web apps!


1. Multiple Body Parameters

The Story

Think of ordering at a restaurant. You don’t just say “food please!” You give the waiter:

  • Your main dish (the pizza)
  • Your drink (the beverage)
  • Dessert (something sweet)

Each is a separate thing, but they all go in one order.

What It Means

FastAPI lets you receive multiple different objects in a single request body. Each object gets its own model!

Simple Example

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Pizza(BaseModel):
    name: str
    size: str

class Drink(BaseModel):
    name: str
    ice: bool

@app.post("/order")
async def create_order(
    pizza: Pizza,
    drink: Drink
):
    return {
        "pizza": pizza,
        "drink": drink
    }

What the Request Looks Like

{
    "pizza": {
        "name": "Margherita",
        "size": "large"
    },
    "drink": {
        "name": "Cola",
        "ice": true
    }
}

The Magic

FastAPI automatically:

  • Knows to look for a pizza key
  • Knows to look for a drink key
  • Validates each separately!
graph TD A["Request Body"] --> B{FastAPI Parser} B --> C["pizza object"] B --> D["drink object"] C --> E["Validate Pizza"] D --> F["Validate Drink"] E --> G["Return Response"] F --> G

2. Embedded Body

The Story

Sometimes your pizza order has extra details inside details. Like:

  • Pizza → has → Crust Details → has → Thickness, Stuffed?

It’s like a Russian nesting doll - objects inside objects!

What It Means

You can nest models inside other models. FastAPI handles all the layers automatically.

Simple Example

from pydantic import BaseModel

class Crust(BaseModel):
    type: str
    thickness: str

class Pizza(BaseModel):
    name: str
    crust: Crust  # Embedded!

@app.post("/pizza")
async def make_pizza(pizza: Pizza):
    return pizza

What the Request Looks Like

{
    "name": "Supreme",
    "crust": {
        "type": "thin",
        "thickness": "5mm"
    }
}

Using Body(embed=True)

Want to wrap a single model in an extra layer?

from fastapi import Body

@app.post("/item")
async def create_item(
    item: Item = Body(embed=True)
):
    return item

Without embed:

{"name": "Pizza", "price": 10}

With embed:

{
    "item": {"name": "Pizza", "price": 10}
}

3. Partial Updates with PATCH

The Story

Your friend ordered a pizza but wants to change just the size—not the whole order!

  • PUT = “Replace everything”
  • PATCH = “Change only what I mention”

It’s like editing a document. Sometimes you fix one typo, not rewrite the whole thing!

Simple Example

from typing import Optional
from pydantic import BaseModel

class PizzaUpdate(BaseModel):
    name: Optional[str] = None
    size: Optional[str] = None
    price: Optional[float] = None

# Fake database
pizzas_db = {
    1: {"name": "Margherita", "size": "M", "price": 12.0}
}

@app.patch("/pizza/{pizza_id}")
async def update_pizza(
    pizza_id: int,
    pizza: PizzaUpdate
):
    stored = pizzas_db[pizza_id]
    update_data = pizza.model_dump(exclude_unset=True)

    for key, value in update_data.items():
        stored[key] = value

    return stored

The Magic: exclude_unset=True

This is the secret sauce! It means:

  • Only get fields the user actually sent
  • Ignore fields they didn’t mention

Request:

{"size": "XL"}

Result: Only size changes. Name and price stay the same!

graph TD A["Original: name=Margherita, size=M, price=12"] --> B["PATCH Request: size=XL"] B --> C["exclude_unset=True"] C --> D["Only update: size"] D --> E["Result: name=Margherita, size=XL, price=12"]

4. Validation Constraints

The Story

Remember our pizza machine? It needs rules:

  • You can’t order 0 pizzas
  • Pizza name can’t be empty
  • Price can’t be negative

These are validation constraints—guardrails that keep data clean!

Common Constraints

from pydantic import BaseModel, Field

class Pizza(BaseModel):
    # String constraints
    name: str = Field(
        min_length=1,      # At least 1 character
        max_length=50      # At most 50 characters
    )

    # Number constraints
    price: float = Field(
        gt=0,              # Greater than 0
        le=1000            # Less than or equal to 1000
    )

    # Integer constraints
    quantity: int = Field(
        ge=1,              # Greater or equal to 1
        lt=100             # Less than 100
    )

Constraint Cheat Sheet

Constraint Meaning Example
gt Greater than gt=0 → must be > 0
ge Greater or equal ge=1 → must be >= 1
lt Less than lt=100 → must be < 100
le Less or equal le=50 → must be <= 50
min_length Minimum string length min_length=3
max_length Maximum string length max_length=100
pattern Regex pattern pattern="^[A-Z]"

Real Example

from pydantic import BaseModel, Field, field_validator

class Order(BaseModel):
    customer_name: str = Field(min_length=2)
    email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+quot;)
    items: int = Field(ge=1, le=10)
    total: float = Field(gt=0)

    @field_validator('customer_name')
    @classmethod
    def name_must_be_alpha(cls, v):
        if not v.replace(' ', '').isalpha():
            raise ValueError('Name must contain only letters')
        return v.title()

What Happens When Validation Fails?

FastAPI returns a 422 Unprocessable Entity error with details:

{
    "detail": [
        {
            "loc": ["body", "price"],
            "msg": "Input should be greater than 0",
            "type": "greater_than"
        }
    ]
}

5. Parameter Models

The Story

Instead of listing every query parameter separately, you can group them into a model. It’s like having a form with sections instead of random questions!

The Old Way (Messy)

@app.get("/pizzas")
async def search_pizzas(
    name: str = None,
    min_price: float = None,
    max_price: float = None,
    size: str = None,
    vegetarian: bool = None
):
    # Gets messy with many params!
    pass

The New Way (Clean!)

from typing import Annotated
from fastapi import Query

class PizzaFilter(BaseModel):
    name: str | None = None
    min_price: float | None = Field(None, ge=0)
    max_price: float | None = Field(None, le=1000)
    size: str | None = None
    vegetarian: bool | None = None

    model_config = {"extra": "forbid"}

@app.get("/pizzas")
async def search_pizzas(
    filters: Annotated[PizzaFilter, Query()]
):
    return {"filters": filters}

Why This Is Better

  1. Organized - All filter params in one place
  2. Reusable - Use same model in multiple endpoints
  3. Validated - Constraints apply automatically
  4. Documented - OpenAPI docs show it clearly
graph TD A["Query Parameters"] --> B["Parameter Model"] B --> C["Validation"] C --> D["Type Conversion"] D --> E["Your Function"] B --> F["Reuse Anywhere"] B --> G["Auto Documentation"]

Full Example

from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing import Annotated

app = FastAPI()

class SearchParams(BaseModel):
    q: str | None = Field(
        None,
        min_length=1,
        description="Search query"
    )
    page: int = Field(
        1,
        ge=1,
        description="Page number"
    )
    limit: int = Field(
        10,
        ge=1,
        le=100,
        description="Items per page"
    )

@app.get("/search")
async def search(
    params: Annotated[SearchParams, Query()]
):
    return {
        "query": params.q,
        "page": params.page,
        "limit": params.limit
    }

Putting It All Together

Here’s a complete example using everything we learned:

from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
from typing import Optional, Annotated

app = FastAPI()

# Embedded model
class Address(BaseModel):
    street: str = Field(min_length=5)
    city: str
    zip_code: str = Field(pattern=r"^\d{5}quot;)

# Main model with validation
class Customer(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    email: str
    address: Address  # Embedded!

class OrderItem(BaseModel):
    pizza_name: str
    quantity: int = Field(ge=1, le=10)
    price: float = Field(gt=0)

# Multiple body parameters
@app.post("/complete-order")
async def create_order(
    customer: Customer,
    items: list[OrderItem]
):
    total = sum(item.price * item.quantity for item in items)
    return {
        "customer": customer,
        "items": items,
        "total": total
    }

# Partial update model
class OrderUpdate(BaseModel):
    status: Optional[str] = None
    notes: Optional[str] = None

@app.patch("/order/{order_id}")
async def update_order(
    order_id: int,
    update: OrderUpdate
):
    changes = update.model_dump(exclude_unset=True)
    return {"order_id": order_id, "updated": changes}

Key Takeaways

Concept What It Does When to Use
Multiple Body Params Accept several objects in one request Complex forms with different sections
Embedded Body Nest models inside models Structured data with sub-objects
PATCH Updates Change only specific fields Edit operations
Validation Constraints Enforce data rules Always! Keep data clean
Parameter Models Group query params Search/filter endpoints

You Did It!

You now understand how FastAPI handles complex request data like a pro! Think of it this way:

  • Multiple Body = Different boxes in one delivery
  • Embedded = Boxes inside boxes
  • PATCH = Edit just one thing
  • Validation = Rules that keep everything correct
  • Parameter Models = Organized forms

Go build something amazing! Your pizza machine awaits!

Loading story...

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.

Interactive Preview

Interactive - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.

Stay Tuned!

Interactive content is coming soon.

Cheatsheet Preview

Cheatsheet - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.

Stay Tuned!

Cheatsheet is coming soon.

Quiz Preview

Quiz - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.

Stay Tuned!

Quiz is coming soon.

Flashcard Preview

Flashcard - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.

Stay Tuned!

Flashcards are coming soon.