Why validation and parsing matter
In a REST API, inputs arrive as untrusted strings (JSON bodies, query parameters, path parameters, headers). Robust validation ensures you reject malformed data early, return consistent error details, and prevent downstream bugs (database errors, security issues, unexpected business logic). FastAPI relies on Pydantic to both parse raw inputs into Python types and validate them against constraints. When validation fails, FastAPI automatically returns a 422 Unprocessable Entity response with structured error information.
Field constraints with Pydantic
Pydantic lets you express common rules directly on fields: length limits, numeric bounds, regex patterns, and more. These constraints are enforced at runtime and also appear in the generated OpenAPI schema.
String constraints: min/max length and regex
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
username: str = Field(
..., min_length=3, max_length=20, pattern=r"^[a-zA-Z0-9_]+$",
description="3-20 chars, letters/numbers/underscore only"
)
display_name: str | None = Field(None, max_length=50)
min_length/max_lengthenforce size constraints.pattern(regex) enforces format. Keep patterns simple and test them.descriptionimproves OpenAPI docs for consumers.
Numeric constraints: bounds and multiples
from pydantic import BaseModel, Field
class ProductCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=80)
price: float = Field(..., gt=0, description="Must be greater than 0")
stock: int = Field(0, ge=0, le=100_000)
discount_percent: int | None = Field(None, ge=0, le=90)
gt/gemean greater-than / greater-or-equal.lt/lemean less-than / less-or-equal.- Use integer types for counts and identifiers when appropriate to avoid float rounding issues.
Collection constraints
from pydantic import BaseModel, Field
class BulkTagUpdate(BaseModel):
tags: list[str] = Field(..., min_length=1, max_length=10)
Here, min_length/max_length apply to the list itself (number of items). If you need per-item rules, combine with strict typing or custom validators.
Strict typing to avoid surprising coercions
By default, Pydantic may coerce some inputs (for example, turning "123" into 123). In many APIs, you want to reject wrong types instead of silently converting them.
Using strict types
from pydantic import BaseModel, Field, StrictInt, StrictStr
class InventoryAdjust(BaseModel):
product_id: StrictInt
reason: StrictStr = Field(..., min_length=3, max_length=100)
delta: StrictInt = Field(..., ge=-1000, le=1000)
With StrictInt and StrictStr, values like "5" (string) will be rejected for an integer field, producing a validation error.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Custom validators for business rules
Field constraints cover many cases, but business rules often require custom logic: cross-field checks, normalization, or conditional requirements.
Normalize and validate a field
from pydantic import BaseModel, Field, field_validator
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=20, pattern=r"^[a-zA-Z0-9_]+$")
@field_validator("username")
@classmethod
def normalize_username(cls, v: str) -> str:
v = v.strip()
return v.lower()
This validator trims whitespace and lowercases the username. If you want to reject leading/trailing spaces instead of normalizing, raise a ValueError with a clear message.
Cross-field validation
from datetime import datetime
from pydantic import BaseModel, Field, model_validator
class PromotionCreate(BaseModel):
starts_at: datetime
ends_at: datetime
percent_off: int = Field(..., ge=1, le=90)
@model_validator(mode="after")
def check_dates(self):
if self.ends_at <= self.starts_at:
raise ValueError("ends_at must be after starts_at")
return self
model_validator is useful when validation depends on multiple fields.
Parsing common input types (datetime, UUID, email)
FastAPI automatically parses many common types when you annotate them in your models or endpoint parameters. This reduces manual parsing and centralizes validation.
Datetime parsing
When a field is annotated as datetime, Pydantic accepts ISO 8601 strings and converts them into datetime objects.
from datetime import datetime
from pydantic import BaseModel
class EventCreate(BaseModel):
title: str
starts_at: datetime
Example JSON body:
{
"title": "Team sync",
"starts_at": "2026-01-16T09:30:00Z"
}
UUID parsing
from uuid import UUID
from pydantic import BaseModel
class OrderRead(BaseModel):
id: UUID
Inputs like "550e8400-e29b-41d4-a716-446655440000" are parsed into UUID objects. Invalid UUID strings produce a 422 error.
Email validation
Pydantic provides an email type that validates format. You may need the optional dependency email-validator installed in your environment for full validation support.
from pydantic import BaseModel
from pydantic import EmailStr
class NewsletterSignup(BaseModel):
email: EmailStr
How validation errors look in FastAPI responses
When validation fails, FastAPI returns a 422 response with a predictable structure. This is helpful for frontend clients because they can map errors to specific fields.
Example: invalid input response
If a client sends username that is too short and an invalid email:
{
"username": "ab",
"email": "not-an-email"
}
FastAPI returns something like:
{
"detail": [
{
"type": "string_too_short",
"loc": ["body", "username"],
"msg": "String should have at least 3 characters",
"input": "ab",
"ctx": {"min_length": 3}
},
{
"type": "value_error",
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"input": "not-an-email"
}
]
}
loctells you where the error occurred (body/query/path) and which field.msgis a human-readable message.typeandctxhelp programmatic handling.
Query parameter validation
Query parameters are strings by default, but FastAPI will parse them into the annotated types and validate constraints. For query constraints, use Query (and similarly Path, Header, Cookie).
Validating pagination and filters
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/products")
def list_products(
q: str | None = Query(None, min_length=2, max_length=50, description="Search term"),
page: int = Query(1, ge=1, description="1-based page number"),
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
min_price: float | None = Query(None, ge=0),
max_price: float | None = Query(None, ge=0),
):
return {
"q": q,
"page": page,
"page_size": page_size,
"min_price": min_price,
"max_price": max_price,
}
Try calling:
/products?page=0(fails becausege=1)/products?page_size=500(fails becausele=100)/products?q=a(fails becausemin_length=2)
Query parsing for UUID and datetime
from datetime import datetime
from uuid import UUID
from fastapi import Query
@app.get("/orders")
def list_orders(
customer_id: UUID = Query(..., description="Customer UUID"),
created_after: datetime | None = Query(None, description="ISO 8601 timestamp"),
):
return {"customer_id": str(customer_id), "created_after": created_after}
If customer_id is not a valid UUID, FastAPI returns a 422 with loc set to ["query", "customer_id"].
Automatic OpenAPI documentation of constraints
Constraints you declare via Field and Query are exported into OpenAPI automatically. This means the interactive docs show:
- Minimum/maximum values for numbers (
minimum/maximum). - Minimum/maximum length for strings (
minLength/maxLength). - Regex patterns (
pattern). - Descriptions and examples (if provided).
Adding examples to improve docs
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
username: str = Field(
..., min_length=3, max_length=20,
examples=["sam_dev", "api_user_01"]
)
For query parameters:
from fastapi import Query
@app.get("/users")
def list_users(role: str | None = Query(None, examples=["admin", "member"])):
return {"role": role}
Refining earlier endpoints to reject invalid input with meaningful details
Assume you already have endpoints that create resources (for example, users and products). The refinement is to tighten models and parameters so invalid input is rejected before it reaches your service layer.
Step 1: Strengthen the request model with constraints and strict types
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field, EmailStr, StrictStr, field_validator
class UserCreate(BaseModel):
email: EmailStr
username: StrictStr = Field(..., min_length=3, max_length=20, pattern=r"^[a-z0-9_]+$")
registered_at: datetime | None = None
referral_code: StrictStr | None = Field(None, min_length=6, max_length=12, pattern=r"^[A-Z0-9]+$")
@field_validator("username")
@classmethod
def username_must_be_lower(cls, v: str) -> str:
if v != v.lower():
raise ValueError("username must be lowercase")
return v
Step 2: Validate query parameters for listing/search endpoints
from fastapi import Query
@app.get("/users")
def list_users(
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
created_after: datetime | None = Query(None),
):
return {"limit": limit, "offset": offset, "created_after": created_after}
Step 3: Validate path parameters with type annotations
from uuid import UUID
@app.get("/users/{user_id}")
def get_user(user_id: UUID):
return {"user_id": str(user_id)}
If a client calls /users/not-a-uuid, FastAPI responds with a 422 error pointing to ["path", "user_id"].
Step 4: Observe and use the error details
When a request fails validation, you get consistent error payloads without writing manual checks. Use these details to:
- Show field-level messages in a UI.
- Log invalid request patterns (for example, repeated malformed UUIDs).
- Guide API consumers by improving
description,examples, and constraints in your schema.