Skip to content

Request Body Interview Questions & Answers

13 questions Updated 2026-06-20 Share:

FastAPI request body interview questions — Pydantic models as bodies, Body(), nested models, multiple bodies, form data, file uploads and embed.

13 of 13

Declare a function parameter typed as a Pydantic BaseModel subclass. FastAPI reads the request body as JSON, parses it, and validates it against the model.

from pydantic import BaseModel
from fastapi import FastAPI

class Item(BaseModel):
    name: str
    price: float
    in_stock: bool = True

app = FastAPI()

@app.post("/items")
async def create_item(item: Item):
    return item
# POST /items  body: {"name": "Widget", "price": 9.99}
# → Item(name='Widget', price=9.99, in_stock=True)

FastAPI returns 422 if the body is missing, malformed JSON, or fails validation.

Rule of thumb: one Pydantic model = one request body; the model name appears in the OpenAPI schema as the expected input shape.

Annotate the body parameter with T | None = None:

class Item(BaseModel):
    name: str
    price: float

@app.patch("/items/{id}")
async def update_item(id: int, item: Item | None = None):
    if item is None:
        return {"updated": False}
    return {"updated": True, "item": item}

A None default makes the body optional; FastAPI won't error if the client sends an empty body. For partial updates prefer a model where all fields are Optional rather than making the whole body optional.

Rule of thumb: for PATCH endpoints, make individual fields optional inside the model rather than making the entire body optional.

Nest a BaseModel inside another BaseModel. FastAPI validates the full nested structure and generates the OpenAPI schema with $ref references.

class Address(BaseModel):
    street: str
    city: str
    postcode: str

class User(BaseModel):
    name: str
    email: str
    address: Address       # nested model

@app.post("/users")
async def create_user(user: User):
    return user
# body: {"name": "Alice", "email": "a@b.com",
#         "address": {"street": "1 Main St", "city": "London", "postcode": "SW1A"}}

Arbitrarily deep nesting is supported; Pydantic validates all levels.

Rule of thumb: prefer deep models over flat ones with address_street, address_city prefixes — nested models are cleaner and reusable across endpoints.

When you declare two Pydantic model parameters, FastAPI expects the body to be a JSON object keyed by parameter name:

class Item(BaseModel):
    name: str
    price: float

class Supplier(BaseModel):
    name: str
    contact: str

@app.post("/items")
async def create_item(item: Item, supplier: Supplier):
    return {"item": item, "supplier": supplier}
# body: {"item": {"name": "Widget", "price": 9.99},
#         "supplier": {"name": "Acme", "contact": "info@acme.com"}}

Rule of thumb: multiple body models wrap themselves under their parameter names automatically; use this to bundle related but distinct payloads in one request.

Use Body(embed=True) on the scalar value. This forces FastAPI to wrap it under its parameter name in the expected JSON object.

from fastapi import Body
from typing import Annotated

class Item(BaseModel):
    name: str

@app.put("/items/{id}")
async def update_item(
    id: int,
    item: Item,
    importance: Annotated[int, Body(ge=1, le=5, embed=True)],
):
    return {"item": item, "importance": importance}
# body: {"item": {"name": "Widget"}, "importance": 3}

Without embed=True, FastAPI would expect importance at the top level of the JSON, which conflicts with item's keys.

Rule of thumb: any singular Body() value alongside a Pydantic model needs embed=True so FastAPI knows to namespace it.

By default (Pydantic v2), extra fields are ignored — they're stripped silently and don't reach the handler.

class Item(BaseModel):
    name: str
    price: float
# body: {"name": "Widget", "price": 9.99, "secret": "ignored"}
# → Item(name='Widget', price=9.99)  — secret dropped

You can change this behaviour via model_config:

from pydantic import BaseModel, ConfigDict

class StrictItem(BaseModel):
    model_config = ConfigDict(extra="forbid")  # 422 if extra fields sent
    name: str
    price: float

Options: "ignore" (default), "allow" (keep in model.model_extra), "forbid" (raise validation error).

Rule of thumb: use extra="forbid" for request bodies to catch client typos early; use extra="ignore" for internal models where schema drift is acceptable.

Type-annotate the body parameter as list[MyModel]:

@app.post("/items/bulk")
async def bulk_create(items: list[Item]):
    return {"count": len(items)}
# body: [{"name": "A", "price": 1.0}, {"name": "B", "price": 2.0}]

FastAPI validates each element against Item. For a top-level list, there is no "outer key" — the raw JSON array is the entire body.

Rule of thumb: use bulk endpoints for batch operations; add a reasonable size limit (len(items) <= 100) inside the handler to prevent abuse.

Use Form() annotation and install python-multipart:

from fastapi import Form
from typing import Annotated

@app.post("/login")
async def login(
    username: Annotated[str, Form()],
    password: Annotated[str, Form()],
):
    return {"user": username}
# Content-Type: application/x-www-form-urlencoded
# body: username=alice&password=secret

You cannot mix Form() and a Pydantic JSON body in the same handler — HTTP only allows one Content-Type per request.

Rule of thumb: use Form() for OAuth2 password flows and HTML <form> submissions; use JSON body everywhere else.

Use UploadFile for the file and Form() for accompanying fields. Both are multipart/form-data.

from fastapi import UploadFile, Form
from typing import Annotated

@app.post("/upload")
async def upload(
    file: UploadFile,
    description: Annotated[str, Form()],
):
    content = await file.read()
    return {
        "filename": file.filename,
        "size": len(content),
        "description": description,
    }

UploadFile wraps a SpooledTemporaryFile; use await file.read() for small files, iterate in chunks for large ones.

Rule of thumb: always await file.seek(0) before re-reading a file that was already partially read elsewhere in the handler.

HTTP 422 Unprocessable Entity with a JSON body listing each failing field:

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "price"],
      "msg": "Field required",
      "input": {"name": "Widget"},
      "url": "https://errors.pydantic.dev/2.0/v/missing"
    },
    {
      "type": "float_parsing",
      "loc": ["body", "price"],
      "msg": "Input should be a valid number",
      "input": "not-a-number"
    }
  ]
}

loc is a breadcrumb path: ["body", "field_name"] for top-level fields, ["body", "nested", "field"] for nested models.

Rule of thumb: parse detail[*].loc in client code to map validation errors back to specific form fields for UX display.

FastAPI expects Content-Type: application/json for Pydantic body parameters. If the client sends a different or missing Content-Type, FastAPI attempts to parse the body as JSON anyway — but tools like Swagger UI always send the header.

POST /items HTTP/1.1
Content-Type: application/json

{"name": "Widget", "price": 9.99}

For form data: application/x-www-form-urlencoded or multipart/form-data. For raw file streams: application/octet-stream with Request body directly.

Rule of thumb: always set Content-Type: application/json explicitly in REST clients — don't rely on default behaviour.

Inject the Request object and call await request.body():

from fastapi import Request

@app.post("/webhook")
async def webhook(request: Request):
    raw = await request.body()       # bytes
    payload = json.loads(raw)
    signature = request.headers.get("X-Signature")
    verify_signature(raw, signature) # HMAC check on raw bytes
    return {"ok": True}

You cannot mix await request.body() with a Pydantic body parameter in the same handler — FastAPI reads the body stream once; body() consumes it before Pydantic can.

Rule of thumb: use raw body access for webhooks that need HMAC verification on the exact bytes; use Pydantic models for everything else.

FastAPI has no built-in body size limit. Enforce it in middleware:

from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

MAX_BODY = 1 * 1024 * 1024  # 1 MB

class LimitBodyMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        if request.headers.get("content-length"):
            if int(request.headers["content-length"]) > MAX_BODY:
                return JSONResponse({"detail": "Body too large"}, status_code=413)
        return await call_next(request)

app.add_middleware(LimitBodyMiddleware)

For production, configure the limit at the reverse proxy (Nginx client_max_body_size or Uvicorn --limit-concurrency) rather than in application code.

Rule of thumb: enforce body size at the reverse proxy level for efficiency; add middleware as a defence-in-depth layer inside the app.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel