Python Request Validation Patterns — Core Concepts
Why request validation matters
Invalid data that slips past the API boundary causes cascading failures: database constraint violations, runtime TypeErrors buried deep in business logic, corrupted state that surfaces hours later. Catching bad data at the entry point is both cheaper and safer.
Layers of validation
Request validation is not a single check — it happens in layers:
Syntactic validation — Is the JSON well-formed? Are required fields present? Is the age field actually a number? This layer catches structural problems.
Semantic validation — Does the value make sense? An age of -5 is syntactically valid (it is a number) but semantically wrong. A start date after an end date is another example.
Business rule validation — Is this email already registered? Does this user have permission to create this resource? This layer requires database lookups or service calls.
Each layer should return clear, specific errors rather than a generic “bad request.”
Pydantic: the Python standard
Pydantic dominates Python request validation. FastAPI uses it natively, and it integrates with Flask, Django, and standalone scripts.
A Pydantic model defines the expected shape:
from pydantic import BaseModel, EmailStr, Field
from datetime import date
class CreateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: EmailStr
birth_date: date
role: str = Field(pattern=r"^(admin|editor|viewer)$")
When FastAPI receives a request, Pydantic validates automatically. Invalid data returns a 422 response with field-level error details — no manual checking needed.
Key validation patterns
Constrained types — Use Field() to set boundaries: min_length, max_length, ge (greater or equal), le, pattern for regex. This replaces manual if-statements.
Custom validators — For rules that go beyond simple constraints:
from pydantic import field_validator
class DateRangeRequest(BaseModel):
start_date: date
end_date: date
@field_validator("end_date")
@classmethod
def end_after_start(cls, v, info):
if "start_date" in info.data and v <= info.data["start_date"]:
raise ValueError("end_date must be after start_date")
return v
Nested models — Complex requests use composition:
class Address(BaseModel):
street: str
city: str
country: str = Field(min_length=2, max_length=2)
class CreateOrderRequest(BaseModel):
items: list[OrderItem]
shipping_address: Address
billing_address: Address | None = None
Discriminated unions — When a request can be one of several types:
from typing import Literal
from pydantic import Discriminator
class CardPayment(BaseModel):
method: Literal["card"]
card_number: str
expiry: str
class BankPayment(BaseModel):
method: Literal["bank"]
account_number: str
routing_number: str
class PaymentRequest(BaseModel):
payment: CardPayment | BankPayment
Path and query parameter validation
Validation applies beyond request bodies. FastAPI validates path parameters and query strings:
from fastapi import Query, Path
@app.get("/users/{user_id}/orders")
async def get_orders(
user_id: int = Path(gt=0),
status: str = Query(default="all", pattern="^(all|pending|shipped)$"),
limit: int = Query(default=20, ge=1, le=100),
):
...
Common misconception
Some developers validate in the business logic layer and skip API-level validation. This is risky because business logic assumes well-typed input. If a string arrives where an integer is expected, the error message will be cryptic and buried in a stack trace instead of a clean 422 at the boundary.
The correct approach: validate structure and types at the API boundary with Pydantic, then validate business rules in the service layer. Both layers are necessary.
Header and cookie validation
Often overlooked, headers need validation too. API keys, authorization tokens, and content types should all be validated at the boundary:
from fastapi import Header
@app.post("/webhooks")
async def receive_webhook(
x_webhook_signature: str = Header(...),
content_type: str = Header(..., pattern="^application/json$"),
):
...
The one thing to remember: Validate in layers — types and structure at the API boundary with Pydantic, business rules in the service layer — and always return specific field-level errors.
See Also
- Python Api Authentication Comparison API keys, JWTs, OAuth, and sessions — four ways Python APIs verify who is knocking at the door.
- Python Api Caching Layers Why Python APIs remember answers to common questions — like a teacher who writes frequent answers on the whiteboard.
- Python Api Error Handling Standards Why good error messages from your Python API are like clear road signs — they tell callers exactly what went wrong and what to do next.
- Python Api Load Testing Testing how many people your Python API can handle at once — like stress-testing a bridge before opening it to traffic.
- Python Api Monitoring Observability How Python APIs keep track of their own health — like a car dashboard that warns you before the engine overheats.