FastAPI Request Body and Pydantic Models
BaseModel. When you use this class as a type hint in your endpoint function, FastAPI automatically: 1. Reads the incoming JSON. 2. Validates it against your schema. 3. Converts types (coercion). 4. Injects a populated Python object into your function. If validation fails, the client receives a 422 error with specific details—no manual if/else validation required.
Defining a Pydantic Model with Type Coercion
A Pydantic model is not just a validator; it's a data transformer. If a client sends a numeric string like "99.99" for a field typed as float, Pydantic will 'coerce' it into a proper Python float automatically.
from fastapi import FastAPI from pydantic import BaseModel, Field from typing import Optional app = FastAPI() class ForgeItem(BaseModel): # Required field: No default provided name: str # Field with constraints and metadata price: float = Field(gt=0, description="Unit price in USD") # Optional with default value is_active: bool = True # Explicitly optional field description: Optional[str] = None @app.post('/forge/items') async def create_artifact(item: ForgeItem): # item is now a fully validated ForgeItem object return {"msg": "Success", "data": item.model_dump()}
Handling Nested Models and Collections
APIs are rarely flat. Pydantic handles recursive validation of nested objects and lists with ease. If even one deeply nested field fails validation, the entire request is rejected, maintaining data integrity.
from fastapi import FastAPI from pydantic import BaseModel, EmailStr app = FastAPI() class Address(BaseModel): street: str city: str zip_code: str class ForgeUser(BaseModel): username: str # EmailStr provides built-in regex validation for emails email: EmailStr address: Address tags: list[str] = [] @app.post('/forge/users') async def register_user(user: ForgeUser): return {"status": "registered", "user": user}
Advanced Logic with Field Validators
Sometimes basic constraints like min_length aren't enough. Use @field_validator for complex cross-field validation or custom business rules that require Python logic.
from fastapi import FastAPI from pydantic import BaseModel, Field, field_validator app = FastAPI() class ForgeProduct(BaseModel): sku: str = Field(min_length=8, max_length=12) inventory_count: int = Field(ge=0) category: str @field_validator('sku') @classmethod def sku_must_be_uppercase(cls, v: str) -> str: if not v.isupper(): raise ValueError('SKU must be all uppercase for tracking purposes') return v @app.post('/forge/inventory') async def update_stock(product: ForgeProduct): return product
🎯 Key Takeaways
- Pydantic models act as the 'Data Contract' between your client and your server.
- Type Coercion: Pydantic intelligently converts compatible data types (e.g., string '1' to int 1) during parsing.
- Recursive Validation: Nested models allow for complex, structured data validation in a single declaration.
- Field Metadata: Use Field() to add description, example, and numeric/string constraints for both validation and Swagger UI.
- Model Dumping: Use
.model_dump()to convert a Pydantic object back into a dictionary for database operations.
Interview Questions on This Topic
- QHow does FastAPI distinguish between a Pydantic model intended for the request body and one intended for query parameters?
- QExplain the 'Data Coercion' lifecycle: What happens internally when a client sends an ISO-formatted string to a field typed as `datetime.datetime`?
- QScenario: You have a high-traffic endpoint receiving 10MB JSON payloads. How would you optimize Pydantic's validation performance?
- QHow would you implement a validator that checks the 'price' field against a 'minimum_discount' field to ensure business logic consistency?
- QWhat is the difference between Pydantic's `BaseModel` and Python's native `@dataclass`, and why does FastAPI prefer the former?
Frequently Asked Questions
What is the difference between Optional[str] and str with a default of None?
In Pydantic v2, Optional[str] (or str | None) indicates that the value itself can be null (JSON null). However, if you don't provide a default value (e.g., field: Optional[str]), the field remains required in the request body—the client must send the key, even if its value is null. To make the key itself optional, you must provide a default: field: Optional[str] = None.
Can I use Pydantic models for response data as well?
Absolutely. Use the response_model parameter in your decorator: @app.get('/', response_model=UserOut). This is a core TheCodeForge best practice as it automatically filters out sensitive data (like passwords) before the JSON is sent back to the client.
How do I handle partial updates (PATCH) with Pydantic?
To allow partial updates, you can create a model where all fields are optional. When processing the request, use model.model_dump(exclude_unset=True). This ensures you only update the fields the user actually sent, rather than overwriting existing data with default values.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.