πUnit 8: Request Body
Introduction
In FastAPI, the request body is typically used to send data from a client to your API. It is often used for creating or updating resources. FastAPI automatically parses the request body based on the type hints you provide in your function parameters.
FastAPI uses Pydantic models to validate incoming data, ensuring that the request body matches the expected structure and data types. If the validation fails, FastAPI automatically returns a 422 Unprocessable Entity
response detailing the validation errors. This greatly simplifies error handling and ensures that your function only processes valid data.
Request Body
Demo 1
Let's implement a simple example of how to define an endpoint that expects a request body in FastAPI:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
is_offer: bool = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
return item
We first declare a data model as a class Item
that inherits from BaseModel
. You should note that the is_offer
is an optional attribute which has a default value is None
. The function create_item
receive a JSON object from a client and then represented as a dict
object in your program.
Since we used Python Type Hints and Pydantic models in the program, we already have data validation. If the data is invalid, the program will return a clear and informative error, indicating exactly where and what the incorrect data is.
To evaluate the program, we use Postman software and create a new POST
request to the API endpoint at http://127.0.0.1:8000/items/
, as shown in Fig. 1.

Demo 2
In this second demonstration program, we will modify the data received from a client and subsequently return the modified data to the client.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
tax: float = 0.0 # Default value for tax
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
item_dict = item.model_dump()
if item.tax:
final_price = item.price + item.tax
item_dict.update({"final_price": final_price})
return item_dict
The method item.dict()
in class BaseModel
is deprecated, use item.model_dump()
instead.
We also use Postman software to evaluate the program, as shown in Fig. 2.

We can further analyze the program's error scenarios, including cases where the tax
is entered as a textual value or when required attributes are missing.
{
"name": "beer",
"price": 50000,
"tax": "five thousand"
}
With the above request body, you will get the following error:
{
"detail": [
{
"type": "float_parsing",
"loc": [
"body",
"tax"
],
"msg": "Input should be a valid number, unable to parse string as a number",
"input": "five thousand",
"url": "https://errors.pydantic.dev/2.6/v/float_parsing"
}
]
}
However, you should note that the below request body is correct (tax
value is represented as a string) :
{
"name": "beer",
"price": 50000,
"tax": "5000"
}
Demo 3: A complex parameter
In this third demo, we combine all three request body, path parameter and query parameter.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
tax: float = 0.0 # Default value for tax
app = FastAPI()
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, query: str = None):
output = {"item_id": item_id, **item.model_dump()}
if query:
output.update({"query": query})
return output
The method item.dict()
in class BaseModel
is deprecated, use item.model_dump()
instead.
To evaluate the program, you should note that:
The
update_item
function now accepts aPUT
method, instead of thePOST
method as in the previous two programs.The
item_id
is a path parameter.The
query
is a query parameter.The
name
,tax
, andprice
are attributes of a request body.The URL to evaluate this API endpoint is
http://127.0.0.1:8000/items/1001?query=update-item-price
Fig. 3 and Fig. 4 shows a demonstration of this API endpoint in Postman software.


Summary
FastAPI provides a powerful and intuitive way to handle request bodies through its seamless integration with Pydantic models. This combination not only ensures that your API endpoints can efficiently parse and validate incoming data but also significantly enhances the developer experience by reducing boilerplate code and automating documentation. By leveraging type hints and Pydantic models, developers can define clear, concise, and self-documenting APIs that enforce data integrity and provide meaningful error messages when data validation fails.
Exercises
1. Create a Student (Body) with Class Path
Scenario: Add a new student to a specific class.
Endpoint:
POST /classes/{class_id}/students
Path params:
class_id: int
Query params (optional):
notify: bool = false
β send a welcome email or not
Request body (JSON):
{ "full_name": "string", "email": "string", "year": 1 }
Tasks:
Define a Pydantic model
StudentCreate
.Validate
email
format and constrainyear
to1..4
.Return
201
with created student plus theclass_id
andnotify
flag.
Example response:
{ "class_id": 101, "student": { "id": 7, "full_name": "Jane Doe", "email": "[email protected]", "year": 1 }, "notified": false }
2. Bulk Insert with Filtering Preview
Scenario: Upload multiple products to a store, with a preview mode.
Endpoint:
POST /stores/{store_id}/products:bulk
Path params:
store_id: int
Query params:
dry_run: bool = true
β A dry-run means simulating the action without actually performing it. If true, validate and show what would happen without saving.category: str | None
β filter the input to only persist products in this category (when notdry_run
)
Example URL:
POST /stores/101/products:bulk?dry_run=true
POST /stores/101/products:bulk?dry_run=false&category=electronics
Request body (JSON array):
[ { "sku": "ABC-1", "name": "Laptop", "price": 1200.0, "category": "electronics" }, { "sku": "ABC-2", "name": "Mouse", "price": 25.0, "category": "accessories" } ]
Tasks:
Define
ProductIn
model withprice > 0
.If
dry_run=true
, return{"valid": true|false, "errors": [...], "count": N}
.If
dry_run=false
, save only items matchingcategory
(if provided) and return the persisted items.
Example dry-run response:
{ "valid": true, "errors": [], "count": 2 }
3. Partial Update (PATCH) with Field Mask
Scenario: Partially update an order belonging to a user.
Endpoint:
PATCH /users/{user_id}/orders/{order_id}
Path params:
user_id: int
,order_id: int
Query params (optional):
fields: str | None
β comma-separated list of fields that are allowed to be updated (e.g.,status,shipping_address
)
Example URL:
PATCH /users/55/orders/2001?fields=notes,shipping_address
Request body (JSON, all optional fields):
{ "status": "processing|shipped|delivered|cancelled", "shipping_address": { "street": "string", "city": "string", "zip": "string" }, "notes": "string | null" }
Tasks:
Create
OrderPatch
with all fields optional.If
fields
is provided, only apply updates to those fields; reject others with422
and a clear message.Enforce valid status transitions (e.g., cannot go from
delivered
βprocessing
).
Example response:
{ "order_id": 33, "updated_fields": [ "status", "notes" ], "order": { "status": "shipped", "shipping_address": { "street": "19, Nguyen Huu Tho, P. Tan Hung", "city": "Ho Chi Minh", "zip": "70000" }, "notes": "Leave at door" } }
4. Search + Complex Body Filters + Pagination
Scenario: Search tickets for a given project using complex criteria + pagination.
Endpoint:
POST /projects/{project_id}/tickets:search
Path params:
project_id: str
Query params:
limit: int = 10
,offset: int = 0
,sort: str = "created_at:desc"
Example URL:
Returns tickets 6β10 assigned to user
u42
, sorted by priority ascending:POST /projects/alpha123/tickets:search?limit=5&offset=5&sort=priority:asc
Request body (JSON filters):
{ "assignees": [ "u123", "u456" ], "status": [ "open", "in_progress" ], "created_range": { "from": "2025-09-01", "to": "2025-09-30" }, "labels": [ "backend", "bug" ], "text": "database timeout" }
Tasks:
Define nested filter models (
DateRange
,TicketSearch
).Validate
limit in 1..100
andsort
format<field>:<asc|desc>
.Apply filters from body, then paginate + sort via query params.
Return
total
,items
, andnext_offset
if more results exist.
Example response:
{ "total": 57, "items": [ { "id": 901, "title": "Fix DB pool", "status": "open" } ], "next_offset": 10 }
5*. Idempotent Payments with Idempotency-Key
Scenario: Process a payment for an invoice with idempotency and mode toggles.
Endpoint:
POST /invoices/{invoice_id}/payments
Path params:
invoice_id: str
Query params:
mode: str = "live"
β"test"
or"live"
controls which ledger to hitretry_on_timeout: bool = true
β allow internal retry on gateway timeouts
Headers to read (simulate in code):
Idempotency-Key: <uuid>
β identical requests with the same key must return the same result without double-charging
Request body (JSON):
{ "method": "card|bank_transfer|wallet", "amount": 125.50, "currency": "USD", "customer": { "id": "cus_123", "email": "[email protected]" }, "metadata": { "order_id": "ORD-8891" } }
Tasks:
Validate
amount > 0
, supportedcurrency
, and required fields permethod
.Respect
mode
query param to select storage bucket (e.g., in-memory dictspayments_test
vspayments_live
).Implement idempotency: if the same
Idempotency-Key
is seen for the sameinvoice_id
, return the previously stored response.If
retry_on_timeout=true
, simulate a transient gateway timeout and an internal retry strategy.Return normalized payment result with status:
succeeded|failed|pending
, and echoidempotency_key
.
Example success response:
{ "invoice_id": "INV-1001", "payment_id": "pay_77abc", "status": "succeeded", "amount": 125000.0, "currency": "VND", "mode": "live", "idempotency_key": "8f1a6a57-6e2c-4b7b-8c59-3b5b2b21a1a2", "metadata": { "order_id": "ORD-8891" } }
Last updated