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

Sponsored

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

  1. Define the model: Create a class inheriting from pydantic.BaseModel with type-annotated fields.
  2. Declare the parameter: In your route, add a parameter with that model as its type. FastAPI treats it as a request body automatically.
  3. FastAPI parses the JSON: When a request arrives, FastAPI reads the body, parses JSON, and validates against the model.
  4. Validation happens: Types are coerced where safe (e.g., "30" to int 30), required fields enforced, nested models validated recursively.
  5. Passes the instance: If valid, your endpoint receives a model instance with attributes like item.name.
  6. Returns errors: If invalid, FastAPI generates a 422 response 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:

  • 422 error with "field required" for a field that exists: Check for case sensitivity (JSON keys must match field names) and leading/trailing spaces. Also, remember that None is a valid value for Optional fields; if you want None to be rejected, use Field(..., min_length=1).
  • Data type coercion surprises: Pydantic converts "123" to int 123 by default. If you want strict typing (reject strings that look like numbers), use ConfigDict(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 422 when pattern is valid: If you use Field(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 Body to 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 Field defaults 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. Always from pydantic import BaseModel.
  • Using a plain dict as 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 with ConfigDict(strict=True) when exact types matter.

Variations

  1. Using dataclasses instead of Pydantic — works for basic validation but lacks JSON Schema generation and automatic coercion.
  2. Using Union types like Union[int, str] for fields that accept multiple types — Pydantic will validate against each in order.
  3. 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.