Home/Learn/FastAPI/Nested Models — Real-World Payloads

Nested Models — Real-World Payloads

Intermediate
Requests & Validation

Models compose: a field typed as another BaseModel nests an object, list[Model] nests an array, and validation runs recursively with error paths like items.1.qty — plus rich types (EmailStr, UUID, datetime) and Enums for free.

Overview

Real payloads are trees: an order has a delivery address and a list of items, each item has its own fields. Pydantic composes naturally — type a field as Address and the JSON must contain a valid Address object; type it list[OrderItem] and every element is validated, with failure locations that index into the array (items → 1 → qty). Rich types raise the floor further: EmailStr, HttpUrl, UUID, datetime, and Decimal parse and validate formats you would otherwise regex by hand, and str-Enums lock fields to fixed vocabularies. Define each nested model once and reuse it across endpoints — the models become your API vocabulary and the docs render them as an interlinked schema.

Objects in Objects, Lists of Objects

Compose models like types. Validation is recursive; the loc path in a 422 walks the tree including list indexes, so clients can highlight exactly the third item's qty field.

Recursive validation with tree-accurate error locations
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class Address(BaseModel):
    line1: str
    city: str
    pincode: str = Field(pattern=r"^[1-9][0-9]{5}$")

class OrderItem(BaseModel):
    sku: str
    name: str
    qty: int = Field(ge=1, le=20)
    price: float = Field(gt=0)

class OrderIn(BaseModel):
    customer: str
    address: Address                       # nested object
    items: list[OrderItem] = Field(min_length=1)    # non-empty array
    coupon: str | None = None

@app.post("/orders")
def place_order(order: OrderIn):
    total = sum(i.qty * i.price for i in order.items)
    return {"city": order.address.city, "items": len(order.items), "total": total}

# Payload:
# {"customer": "Ravi",
#  "address": {"line1": "12 FC Road", "city": "Pune", "pincode": "411005"},
#  "items": [{"sku": "S1", "name": "Kurta", "qty": 2, "price": 799},
#            {"sku": "S2", "name": "Mojari", "qty": 0, "price": 1299}]}
#
# → 422: {"loc": ["body", "items", 1, "qty"],
#         "msg": "Input should be greater than or equal to 1"}
#   ← the SECOND item's qty, pinpointed. Frontend can highlight that row.

Rich Types and Enums — Stop Writing Regexes

EmailStr, HttpUrl, UUID, datetime, and Decimal validate formats properly (install email-validator for EmailStr). str-Enums constrain vocabulary fields and show as dropdowns in /docs.

EmailStr, UUID, Decimal, datetime, Enum — formats handled properly
from datetime import datetime
from decimal import Decimal
from enum import Enum
from uuid import UUID
from pydantic import BaseModel, EmailStr, HttpUrl

class PaymentMode(str, Enum):
    upi = "upi"
    card = "card"
    netbanking = "netbanking"
    cod = "cod"

class PaymentIn(BaseModel):
    txn_id: UUID                          # "6fa0f7…-…" parsed to UUID
    email: EmailStr                       # real email validation
    amount: Decimal                       # exact money math — never float in ledgers
    mode: PaymentMode                     # only the 4 values above
    receipt_url: HttpUrl | None = None    # scheme + host validated
    paid_at: datetime                     # "2026-07-11T14:30:00+05:30" → aware dt

p = PaymentIn(
    txn_id="6fa0f7f4-3c1e-4c8e-9b2a-1e5d3c7a9b01",
    email="asha@nitk.edu.in",
    amount="1499.50",                     # string in → Decimal("1499.50")
    mode="upi",
    paid_at="2026-07-11T14:30:00+05:30",
)
print(p.paid_at.tzinfo)                   # timezone-aware, IST offset kept
print(p.model_dump_json())                # UUID/Decimal/datetime serialize cleanly

# mode="wallet" → 422 "Input should be 'upi', 'card', 'netbanking' or 'cod'"
# In /docs, mode renders as a dropdown — the Enum IS the documentation.

Key Points to Remember

  • 1Nest by typing: field: Address, items: list[OrderItem], recursive validation free
  • 2422 loc paths walk the tree with list indexes — clients can pinpoint bad rows
  • 3EmailStr/HttpUrl/UUID/datetime/Decimal replace hand-rolled format regexes
  • 4str-Enum fields lock vocabulary and render as dropdowns in /docs

Interview Questions

Sign in to ask Aria
1

Model a food-delivery order payload: restaurant, items with quantities, delivery address.

MediumSwiggy
2

Why Decimal instead of float for the amount field in a payments API?

MediumRazorpay
3

One item in a 50-item order payload is invalid — what exactly does the client receive?

MediumAmazon

Ask Aria about Nested Models — Real-World Payloads

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…