FastAPI / Basic: HTTP and typed boundaries / Lesson 3
Request bodies and Pydantic validation
Turn JSON into a narrow validated command while distinguishing parsing, conversion, field constraints, and cross-field rules.
On this page
Lesson goals
- Explain the assumptions and boundaries of request bodies and pydantic validation.
- Interpret both complete examples and justify their observed outputs.
- Diagnose the listed failures and apply the worked solutions while keeping identity, state, and lifecycle ownership explicit.
A model defines a boundary, not a database
A Pydantic BaseModel declares fields and validation behavior for application data. FastAPI can use it to parse a request body, report invalid inputs, and describe the API schema. The model does not persist data, authorize a caller, or enforce a database transaction. Keep request commands separate from internal records and public responses so a client cannot set privileged properties accidentally. The first example accepts only name and quantity and forbids unknown fields. Returning a newly validated object is safer than copying an arbitrary request dictionary into a persistence model with additional internal attributes.
Know when conversion is part of the contract
Pydantic normally performs supported conversions, such as interpreting some numeric text as an integer. That is a documented behavior, not a claim that all arbitrary values are accepted or that every field is strict. Use strict types or configuration when the API requires exact input types, and test the JSON and Python entry points relevant to the application. The first example deliberately converts quantity from text and checks its positive bound. Decide whether that is suitable for the public contract instead of assuming conversion is always either helpful or forbidden. Missing, null, empty, and wrong-type values deserve separate cases.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field
class Item(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
quantity: int = Field(gt=0)
app = FastAPI()
@app.post("/items")
def create(item: Item): return item.model_dump()
with TestClient(app) as client:
print(client.post("/items", json={"name": "pen", "quantity": "2"}).json())
print(client.post("/items", json={"name": "pen", "quantity": 2, "admin": True}).status_code)
{'name': 'pen', 'quantity': 2}
422
Supported conversion yields integer two. The unknown admin property is rejected with 422 instead of crossing the validated boundary.
Validate relationships explicitly
A field bound cannot express every relationship between fields. A booking window may require end to exceed start even when both values are individually valid integers. A model validator can express that rule and return the validated instance. The second example combines strict integer inputs with a cross-field check and displays a stable error type for invalid ordering. Do not perform remote database lookups, send messages, or mutate global state inside validation as an unnoticed side effect. Validation may run in more than one context, and a rejected command should not have partially changed the world.
from pydantic import BaseModel, ConfigDict, ValidationError, model_validator
class Window(BaseModel):
model_config = ConfigDict(strict=True)
start: int
end: int
@model_validator(mode="after")
def ordered(self):
if self.end <= self.start:
raise ValueError("end must exceed start")
return self
print(Window(start=1, end=3).model_dump())
try:
Window(start=3, end=1)
except ValidationError as error:
print(error.errors()[0]["type"])
try:
Window(start="1", end=3)
except ValidationError as error:
print(error.errors()[0]["type"])
{'start': 1, 'end': 3}
value_error
int_type
Valid ordering produces a model. Reversed ordering gives value_error, and numeric text violates strict integer input with int_type. No external check or side effect occurs.
Keep serialization and schema deliberate
Pydantic v2 uses model_dump and model_validate for common operations. Older examples using deprecated APIs or configuration styles may need migration and should not be copied uncritically. Python representation and JSON representation can differ for dates, decimals, and other supported types; use the appropriate serialization mode. Aliases and extra-field policy also affect the public contract. A schema documents declared constraints but does not prove implementation authorization, byte limits, or concurrency safety. Review generated schema and actual requests together, especially after a dependency upgrade or a change in model configuration.
Make invalid data actionable and safe
FastAPI normally returns a request-validation response before the handler runs when declared input is invalid. Tests should assert status and relevant field locations or error types rather than expose a full submitted body in logs. A domain failure after valid parsing is a separate decision. The examples use only synthetic data and isolated clients or direct model calls; no database, network service, or real identity is involved. Cover wrong shape, extra fields, nullability, constraints, strictness, and cross-field rules. The purpose is an explicit accepted-data contract, not a large collection of decorators that nobody can explain.
Common errors and fixes
Unknown client fields become internal attributes
- Why it happens
- The raw body was copied without a narrow command model.
- How to fix it
- Validate an allowlisted model and construct internal state deliberately.
- Prevent it next time
- Test privilege-shaped and extra fields.
Numeric text is accepted unexpectedly
- Why it happens
- Default conversion was mistaken for strict validation.
- How to fix it
- Choose and test explicit strictness where required.
- Prevent it next time
- Document the accepted public input forms.
Valid fields combine into an invalid command
- Why it happens
- A cross-field invariant was never checked.
- How to fix it
- Add a pure model or domain-level relationship rule.
- Prevent it next time
- Test boundary combinations, not only fields independently.
Check your understanding
1. A model permits start and end as positive integers. Does that prove end is later?
Show worked solution
No. Independent positivity does not express ordering. Validate the relationship explicitly, then test equal, reversed, and valid boundaries. Keep the validation free of persistence or external effects so rejection cannot leave partial state changes.
2. Why should a request model exclude an internal is_admin property?
Show worked solution
The client must not select its own privilege simply by submitting a structurally valid field. Keep identity and authorization decisions on trusted server state, and use a separate input model that contains only accepted command data.
Key takeaways
- A Pydantic BaseModel declares fields and validation behavior for application data.
- A field bound cannot express every relationship between fields.
- FastAPI normally returns a request-validation response before the handler runs when declared input is invalid.