Skip to content

Defining Request Bodies with Pydantic


🎯 What You’ll Learn

  • What request bodies are and when to use them
  • How to define request schemas using Pydantic models
  • How FastAPI automatically validates and documents request bodies

📦 What Is a Request Body?

A request body is the part of an HTTP request that contains data sent by the client — typically in JSON format. It’s used in methods like POST, PUT, and PATCH to create or update resources.

Example:

{
  "title": "Buy milk",
  "completed": false
}

This is different from:

  • Path parameters: part of the URL
  • Query parameters: part of the URL after ?

🧠 Why Use Pydantic?

FastAPI uses Pydantic to define and validate request bodies. Pydantic models:

  • Define the expected structure of incoming data
  • Automatically validate types and required fields
  • Generate clear error messages if validation fails
  • Power the OpenAPI docs (Swagger UI and ReDoc)

🛠️ Step-by-Step: Defining a Request Body

Let’s build a simple API that accepts a task object via POST.

📄 main.py

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Task(BaseModel):
    title: str
    completed: bool

@app.post("/tasks")
def create_task(task: Task):
    return {"message": "Task created", "task": task}

🔍 Breakdown

  • Task is a Pydantic model that defines the expected JSON structure.
  • FastAPI automatically:
    • Parses the incoming JSON
    • Validates the types (str, bool)
    • Injects the validated task object into the function

🧪 Try It Out

Run the server:

uvicorn main:app --reload

Then visit http://127.0.0.1:8000/docs and test the /tasks endpoint.

Example request body:

{
  "title": "Buy milk",
  "completed": false
}

Response:

{
  "message": "Task created",
  "task": {
    "title": "Buy milk",
    "completed": false
  }
}

❌ What Happens If Validation Fails?

Try sending this:

{
  "title": 123,
  "completed": "nope"
}

FastAPI returns a 422 Unprocessable Entity error:

{
  "detail": [
    {
      "loc": ["body", "title"],
      "msg": "str type expected",
      "type": "type_error.str"
    },
    {
      "loc": ["body", "completed"],
      "msg": "value could not be parsed to a boolean",
      "type": "type_error.bool"
    }
  ]
}

This is incredibly helpful for debugging and frontend integration.


🧩 Optional Fields and Defaults

You can make fields optional or give them default values:

class Task(BaseModel):
    title: str
    completed: bool = False
    description: str | None = None
  • completed defaults to False if not provided
  • description is optional (None means it can be missing)

🧠 Practice Challenge

Create a model called User with:

  • username: string
  • email: string
  • is_active: boolean (default True)
  • bio: optional string

Then create a POST /users endpoint that accepts this model and returns the user data.