Pydantic Request Bodies in FastAPI
Understand how to use Pydantic models for request bodies in FastAPI. Learn to define schemas, validate data, and handle errors in this hands-on lesson.
Focus: understanding request bodies with pydantic
Your API endpoint is a locked door. It accepts path parameters and query strings, but how do you safely accept a payload — a JSON body with structured data, nested objects, and required fields — without writing tedious if statements and manual type checks? That's the pain this lesson removes. In FastAPI, understanding request bodies with Pydantic transforms you from a route handler who parses raw JSON to an API designer who declares what data looks like, and lets the framework handle validation, serialization, and error responses automatically. If you've ever wrestled with request.json() and then manually validating keys, this lesson will show you the elegant, declarative way.
The problem this lesson solves
Every real-world API needs to accept data from clients: a new user, a product order, a blog post. Query parameters and path variables are fine for simple identifiers, but they fall apart for anything structured. Attempting to send a list of items through a query string is an exercise in frustration — URL encoding, comma-separated hacks, and no type safety.
The naive approach uses the raw Request object:
from fastapi import Request
@app.post("/users/")
async def create_user(request: Request):
data = await request.json() # dict, no validation
name = data.get("name")
email = data.get("email")
if not name or not email:
return {"error": "Missing fields"}
return {"name": name, "email": email}
This works, but it's brittle. There's no automatic type coercion (the client might send age as a string "30" when you need int 30), no nested validation, no generated docs, and you must manually write error handling for every field. If your API grows to tens of endpoints, the boilerplate explodes and bugs creep in.
FastAPI merges Python's type hints with Pydantic, a data validation library, to solve this elegantly. By the end of this lesson, you'll declare your request body as a Pydantic model, and FastAPI will handle validation, convert data types, and return pristine 422 errors — all automatically.
Core concept / mental model
Pydantic models are classes that inherit from BaseModel and use type annotations to define the shape of data. Think of them as contracts — you describe what a valid request body must look like, and Pydantic enforces it at the door.
When you define a function parameter typed as a Pydantic model, FastAPI reads the JSON body of an incoming request, parses it, validates it against your model, and passes you a fully typed Python object. Invalid data? FastAPI returns a detailed 422 response automatically.
Here's the mental picture: imagine a customs officer at a border. The officer (FastAPI) knows the entry requirements (your Pydantic model). When a traveler (the JSON request) arrives, the officer checks every document (field) — if something's missing or invalid, the traveler is sent back with a precise list of problems. No exceptions.
You can also think of it as a strongly typed dictionary. A plain Python dict lets you access data['name'] but doesn't promise the key exists or has a specific type. A Pydantic instance guarantees it.
How it works step by step
- Define the model: Create a class inheriting from
pydantic.BaseModelwith type-annotated fields. - Declare the parameter: In your route, add a parameter with that model as its type. FastAPI treats it as a request body automatically.
- FastAPI parses the JSON: When a request arrives, FastAPI reads the body, parses JSON, and validates against the model.
- Validation happens: Types are coerced where safe (e.g.,
"30"toint 30), required fields enforced, nested models validated recursively. - Passes the instance: If valid, your endpoint receives a model instance with attributes like
item.name. - Returns errors: If invalid, FastAPI generates a
422response listing each problem field and reason.
This flow is fully synchronous under the hood but works asynchronously in your endpoints — no manual parsing.
Hands-on walkthrough
Let's build a simple API for managing items in a store. First, ensure FastAPI and Pydantic are installed (Pydantic ships with FastAPI, but you can install explicitly):
pip install fastapi "uvicorn[standard]"
Now create a file main.py:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: bool = False # optional with default
tax: float | None = None # optional nullable
@app.post("/items/")
async def create_item(item: Item):
price_with_tax = item.price * (1 + item.tax) if item.tax else item.price
return {"message": f"{item.name} created", "price": price_with_tax}
Run with uvicorn main:app --reload. Send a valid request:
curl -X POST http://localhost:8000/items/ -H "Content-Type: application/json" -d '{"name": "Laptop", "price": 999.99}'
Expected output:
{"message":"Laptop created","price":999.99}
Now send an invalid one — a missing required field:
curl -X POST http://localhost:8000/items/ -H "Content-Type: application/json" -d '{"price": "not-a-number"}'
You'll get a 422 response like:
{
"detail": [
{
"type": "missing",
"loc": ["body", "name"],
"msg": "Field required",
"input": {"price": "not-a-number"}
}
]
}
Notice how Pydantic reported missing name even though the price was also invalid. FastAPI collects all validation errors at once.
Now let's use Field for constraints:
from pydantic import Field
class Item(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
price: float = Field(..., gt=0, le=10000)
tags: list[str] = []
Now a request with a negative price or empty name gets rejected with a clear message. You can also nest models:
class User(BaseModel):
name: str
email: str
class Order(BaseModel):
user: User
items: list[Item]
FastAPI can handle deeply nested structures without extra code.
Compare options / when to choose what
FastAPI gives you several ways to declare parameters; knowing when to use each is crucial.
| Parameter type | Where data lives | When to use |
|---|---|---|
| Path parameter | URL path segment | Identifying a resource (e.g., /items/{item_id}) |
| Query parameter | URL query string | Simple filters, pagination, optional flags |
| Request body (Pydantic) | JSON body | Complex, structured data; creating/updating resources |
| Header/Cookie | Headers/Cookies | Auth tokens, session IDs |
Choose Pydantic models when the data has multiple fields, nested structures, or validation requirements. Use query parameters for simple, flat, optional inputs. Mixing them is common — e.g., GET /users/?page=1 (query) versus POST /users/ (body).
Alternative approaches include using dict directly, but you lose validation. Using dataclasses works for basic validation, but Pydantic offers richer features (JSON Schema generation, coercion, nested models). For most FastAPI apps, Pydantic is the default and best choice.
Troubleshooting & edge cases
Even with Pydantic, you'll hit common pitfalls. Here's how to fix them:
422error with"field required"for a field that exists: Check for case sensitivity (JSON keys must match field names) and leading/trailing spaces. Also, remember thatNoneis a valid value forOptionalfields; if you wantNoneto be rejected, useField(..., min_length=1).- Data type coercion surprises: Pydantic converts
"123"toint 123by default. If you want strict typing (reject strings that look like numbers), useConfigDict(strict=True). This is useful when clients might send inconsistent types. - Nested validation errors: If a nested model fails, the error message includes the path like
body -> user -> email. Use that to pinpoint the problem. - Unexpected
422when pattern is valid: If you useField(pattern=...), Pydantic uses regex internally. Make sure your regex is correct and applied to the string field. - Reading body in a GET request: While you can add a
Bodyto GET, it's unconventional and many clients won't send one. Stick to POST, PUT, or PATCH for bodies. - Inheritance issues: If you subclass a model and change field types, Pydantic v2 may warn about overrides. Use
Fielddefaults carefully. - Large payloads: For body size limits, FastAPI doesn't enforce them by default. Use a middleware or proxy for production.
What you learned & what's next
You now understand how Pydantic request bodies work in FastAPI — you can define structured schemas, validate data automatically, and handle errors gracefully. You learned the mental model of models as contracts, how to declare them in routes, and how to troubleshoot common issues.
This directly connects to your next lesson in the track, which will cover response models — using Pydantic to shape what your API returns. The same schema principles apply, but now you control the output, ensuring consistent API responses and data hiding.
From here, you're ready to build CRUD operations with full validation. Keep experimenting — add more fields, nested models, and constraints to deepen your understanding.
Pro tip: Always view the automatic interactive docs at
/docs— you'll see your Pydantic model rendered as a JSON schema, and you can test requests right from the browser.
Practice recap
Create a new endpoint that accepts a User model with name, email, and a nested Address model (street, city, zip). Add validation for the zip code (e.g., 5 digits). Test with valid and invalid payloads via curl, observing the 422 responses. Then switch to strict mode and see how string numbers are rejected.
Common mistakes
- Forgetting to import
BaseModel— you'll get a NameError. Alwaysfrom pydantic import BaseModel. - Using a plain
dictas a parameter type — FastAPI won't treat it as a request body, leading to unexpected behavior. Use a Pydantic model instead. - Assuming query parameters and body parameters are interchangeable — they are different locations in the HTTP request; a model parameter always reads from the JSON body.
- Ignoring Pydantic's type coercion — accepting a string
"30"as an int can mask client bugs. Use strict mode withConfigDict(strict=True)when exact types matter.
Variations
- Using
dataclassesinstead of Pydantic — works for basic validation but lacks JSON Schema generation and automatic coercion. - Using
Uniontypes likeUnion[int, str]for fields that accept multiple types — Pydantic will validate against each in order. - Defining models with
ConfigDict(extra="forbid")to reject unknown fields — useful for strict API contracts.
Real-world use cases
- E-commerce checkout: receive nested order data with user info and line items, validated automatically before processing payment.
- User registration endpoints: validate email format, password length, and unique constraints with Pydantic fields.
- Internal microservice API: accept structured job definitions with required config fields, auto-generated OpenAPI docs for other teams.
Key takeaways
- Pydantic models define the structure and validation rules for request bodies.
- FastAPI automatically parses JSON body and validates it against your model, returning 422 errors on failure.
- Use type hints and Field constraints to enforce required, optional, and bounded fields.
- Nested models allow complex, hierarchical request bodies without extra code.
- Choose body models for structured data; query parameters for simple filters and options.
- Inspect the /docs endpoint to see your schemas rendered and test interactions.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.