Validating Types, Defaults, Constraints
Learn how to validate types, defaults, and constraints in FastAPI. This step-by-step tutorial shows you how to enforce data integrity, handle edge cases, and apply these concepts in a hands-on exercise.
Focus: validating types, defaults, and constraints
Ever shipped an API endpoint that silently accepted a negative age, a string where a number was expected, or a missing field that crashed your database query? Without explicit validation, your FastAPI app is a ticking time bomb — data corruption, cryptic 500 errors, and frustrated frontend developers. In this lesson, you’ll learn how to use FastAPI’s built-in validation powered by Pydantic to enforce types, set defaults, and apply constraints, so your API rejects bad data before it ever touches your business logic.
The problem this lesson solves
When you build an API, every field in a request body, query parameter, or path parameter is a potential point of failure. Consider this naive endpoint:
from fastapi import FastAPI
app = FastAPI()
@app.post("/items/")
async def create_item(name: str, price: float):
# Imagine saving this to a database...
return {"name": name, "price": price}
If a client sends {"name": "Laptop", "price": "not-a-number"}, FastAPI will raise a validation error by default — that’s good. But what if they send {"name": 123, "price": -10}? FastAPI will coerce 123 to "123" and accept a negative price. Your database may reject it, or worse, your application logic might not expect it. The problem is that type hints alone are not enough to enforce domain rules. You need constraints — minimums, maximums, regex patterns, length limits — and defaults for optional fields. This lesson solves exactly that: how to declare validation rules so your API is predictable, self-documenting, and resilient.
By the end, you’ll be able to write endpoints that reject invalid payloads with clear, automatic 422 error responses, while still allowing flexible input with smart defaults.
Core concept / mental model
Think of FastAPI as a secretary at the door of your application. The secretary reads the type hints you’ve written on your function parameters and checks every incoming request against them. But the secretary also checks a list of extra rules — constraints — that you’ve attached to those parameters. If something doesn’t match, the secretary returns a polite rejection slip (a 422 Unprocessable Entity response) listing exactly what went wrong.
This secretary is built on Pydantic, a data validation library that turns your Python type hints into a full validation engine. When you write name: str, Pydantic creates a Field that expects a string. When you write price: float = Field(gt=0), Pydantic knows the value must be a float greater than zero. The same mechanism applies to query and path parameters through FastAPI’s Query and Path classes.
Key definitions:
- Type validation – enforcing that a value is of a specific Python type (e.g.,
int,str,float,bool,list). - Defaults – a value used when the client doesn’t supply the field. In Python, you set
= valuein the function signature. - Constraints – additional rules like
gt=0(greater than),le=100(less than or equal),min_length=3,max_length=50, orpattern="^[A-Z]$".
Mental model in a diagram:
Client request
↓
FastAPI reads your path/query/body parameter declarations
↓
Pydantic validates type, applies constraints, fills defaults
↓
Pass or reject (422 with detailed error JSON)
↓
Your endpoint function runs with clean, validated data
Think of your endpoint signature as the contract with the client. Every parameter you declare is a promise: “If you send this, I’ll handle it correctly.” Validation is how you enforce that promise.
How it works step by step
When you declare a parameter in FastAPI, the framework follows a predictable sequence:
- Extract the value – From the path, query string, request body, headers, or cookies, depending on how you declare the parameter.
- Check the type – Pydantic coerces the raw input to the declared Python type (e.g., a string "42" becomes integer 42).
- Apply constraints – If you’ve added
Fieldconstraints likegt=0ormax_length=10, Pydantic checks whether the value satisfies them. - Apply defaults – If the parameter is optional and the client didn’t send a value, FastAPI substitutes the default you’ve assigned.
- Return the validated value – Your endpoint function receives a clean, typed value. If anything fails, FastAPI stops and returns a 422 response with a structured error list.
Let’s trace through a concrete example. Declare an endpoint that creates a product with a required name, optional description, and a price that must be positive:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Product(BaseModel):
name: str = Field(min_length=1, max_length=50)
description: str = Field(default="", max_length=500)
price: float = Field(gt=0, le=10000)
in_stock: bool = True
@app.post("/products/")
async def create_product(product: Product):
return {"product": product.model_dump()}
When a client sends {"name": "Coffee Maker", "price": 39.99}:
descriptionandin_stockare missing, so defaults are used (""andTrue).namepasses min/max length checks.priceis a positive float within the allowed range.- The endpoint receives a
Productinstance with all fields populated.
If the client sends {"name": "X", "price": -5}, FastAPI returns a 422 with errors like:
{
"detail": [
{
"loc": ["body", "price"],
"msg": "ensure this value is greater than 0",
"type": "greater_than"
}
]
}
## Hands-on walkthrough
Let’s build a complete example you can run right now.
### Step 1: Set up your environment
Create a new directory and install FastAPI with `uvicorn`:
```bash
pip install fastapi uvicorn
Step 2: Define a model with type validation, defaults, and constraints
Create a file main.py:
from typing import Optional
from fastapi import FastAPI, Query, Path
from pydantic import BaseModel, Field
app = FastAPI()
class User(BaseModel):
username: str = Field(..., min_length=3, max_length=20, pattern="^[a-zA-Z0-9_]+$")
age: int = Field(..., ge=0, le=130)
email: str = Field(..., max_length=100)
is_active: bool = True
tags: list[str] = Field(default=[], max_length=5)
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0, le=10000)
stock: int = Field(default=0, ge=0)
Notice the ellipsis ... — this means the field is required and has no default. For optional fields, you provide a default value.
Step 3: Create endpoints that use these models
@app.post("/users/")
async def create_user(user: User):
return {"username": user.username, "age": user.age, "is_active": user.is_active}
@app.get("/products/{product_id}")
async def get_product(
product_id: int = Path(..., ge=1),
include_details: bool = Query(default=False),
):
return {"product_id": product_id, "include_details": include_details}
Step 4: Run and test
Start the server:
uvicorn main:app --reload
Now try different requests in your browser or with curl:
curl -X POST http://localhost:8000/users/ -H "Content-Type: application/json" -d '{"username": "johndoe", "age": 30, "email": "john@example.com"}'
Expected response:
{"username": "johndoe", "age": 30, "is_active": true}
Now send invalid data:
curl -X POST http://localhost:8000/users/ -H "Content-Type: application/json" -d '{"username": "jd", "age": 200, "email": "j@"}'
You’ll get a 422 with three detailed errors — one for username too short, one for age too high, one for invalid email format (actually, email is not validated as an email by default, but it’s still accepted; we’ll fix that later).
Step 5: Add email validation with a Pydantic type
Pydantic has built-in types like EmailStr that enforce email format. Install the email-validator package:
pip install email-validator
Update your User model:
from pydantic import BaseModel, Field, EmailStr
class User(BaseModel):
username: str = Field(..., min_length=3, max_length=20, pattern="^[a-zA-Z0-9_]+$")
age: int = Field(..., ge=0, le=130)
email: EmailStr
is_active: bool = True
tags: list[str] = Field(default=[], max_length=5)
Now a bad email like "j@" will fail with a clear message.
Expected output for valid data
After restarting, POST a valid user and you’ll see the same success response. Try an invalid email and you’ll get a 422 with a message like “value is not a valid email address”.
Compare options / when to choose what
When you need to validate request data, you have several options. Here’s a comparison:
| Feature | Pydantic Field/BaseModel |
FastAPI Query/Path/Body |
Custom validators |
|---|---|---|---|
| Use case | Request bodies, complex models | Query/Path params, simple checks | Cross-field or complex logic |
| Declarative | Yes | Yes | Yes, with @validator or field_validator |
| Type coercion | Yes | Yes | No |
| Defaults | Yes | Yes | No |
| Constraints (gt, ge, etc.) | Yes | Yes | Yes |
| Reusability | High | Low | High |
| Complexity | Low | Low | Medium |
When to use what?
- For query and path parameters, use
QueryandPath— they give you the same constraints (ge,le,min_length, etc.) but are less reusable across endpoints. - For request bodies, define a Pydantic
BaseModelwithField— this separates concerns and allows you to reuse the model in multiple endpoints and even in your own code. - If you need to validate
start < endor ensure alisthas at least one item with a certain property, use custom validators (covered in a later lesson).
Here’s an example of using Query:
@app.get("/items/")
async def list_items(
page: int = Query(1, ge=1),
per_page: int = Query(10, ge=1, le=100),
):
return {"page": page, "per_page": per_page}
And here’s a Path example:
@app.get("/users/{user_id}")
async def get_user(user_id: int = Path(..., ge=1)):
return {"user_id": user_id}
Both approaches return the same error structure, but the BaseModel pattern is preferred for anything larger than a single parameter because it’s modular and testable.
Pro tip: Use
Query(..., ge=1)to make a query parameter required. The ellipsis is your 🚨 'no default' marker — without it, the parameter is optional.
Troubleshooting & edge cases
Here are the most common issues you’ll run into, with concrete fixes:
1. “Missing required parameter” errors
If you see a 422 saying field required for a field you thought you provided, check that:
- You spelled the JSON key exactly as the field name (case-sensitive).
- For query parameters, the parameter is actually required (no default) or you’ve passed
.... - The client is sending a body when you declared it as a query parameter.
2. Type coercion surprises
FastAPI is lenient by design. A string "42" becomes integer 42, a string "true" becomes boolean True. If you want to disallow coercion, set strict=True in Field:
age: int = Field(..., strict=True)
Now a string "42" will fail, even though it’s convertible.
3. Negative numbers sneaking through
Without ge=0, your endpoint might accept a negative stock. Always add ge=0 for quantities, and gt=0 for prices or IDs.
4. Default for list — beware mutable defaults
You cannot use a mutable default like tags: list = [] directly in Python (it’s a common bug). Pydantic handles it for you if you write Field(default=[]), but be careful if you ever manually assign a default in a regular function.
5. Email validation not working
If you try EmailStr and get a ModuleNotFoundError, you must install email-validator. If you forget, you’ll get an ImportError when starting the app.
6. Regex pattern errors
If you use pattern with a raw string, make sure it’s valid — you could get a 500 instead of a 422 if the pattern is malformed. Test your regex separately.
7. Field vs Query namespace clash
If you import both Field from Pydantic and Query from FastAPI, remember: Field is for Pydantic models, Query is for function parameters. Mixing them up leads to confusing errors.
What you learned & what's next
You now understand how to validate types, set defaults, and apply constraints in FastAPI. You’ve seen how Pydantic models with Field enforce rules on request bodies, how Query and Path do the same for query and path parameters, and how to troubleshoot common validation failures. You also learned the mental model of FastAPI as a validating gatekeeper.
Key skills acquired:
- Declaring required vs. optional fields with
...vs. defaults. - Applying
gt,ge,le,min_length,max_length, andpatternconstraints. - Using Pydantic types like
EmailStrfor richer validation. - Understanding when to use
BaseModel,Query, orPath.
What’s next: In the next lesson, you’ll dive into custom validation and model validators — you’ll learn how to enforce complex rules that span multiple fields (e.g., start < end) and how to validate data with side effects. That’s where real-world business logic comes to life.
Pro tip: Always document your constraints in your OpenAPI schema — FastAPI does it automatically. This gives your frontend team a live contract to follow.
Now, go ahead and add validation to your existing endpoints. Happy coding!
Practice recap
Create a new endpoint called /reviews/ that accepts a Review model with a required product_id (int, ge=1), a rating (float, ge=0, le=5), and an optional comment (str, default , max_length=1000). Test it with invalid data (e.g., rating 6) and observe the 422 response. Then change the comment to be required and see the difference. Great practice for applying constraints!**
Common mistakes
- Forgetting to install
email-validatorwhen usingEmailStr– you’ll get anImportErrorat app startup. - Using
[]as a mutable default in Pydantic – while Pydantic handles it, it’s a bad habit; always useField(default_factory=list)in complex cases. - Confusing
Field(for Pydantic models) withQuery/Path(for function parameters) – leads to import errors or silent misbehavior. - Omitting constraints like
ge=0for quantities andgt=0for prices, only to realize negative values break your DB layer.
Variations
- Use
typing.Optionalordefault=Nonefor optional fields instead of a fill-in default like empty string. - Use Pydantic’s
StrictBool,StrictInt, orconint,confloat,constrfor stricter or constrained types. - Leverage
model_validator(new in Pydantic v2) for cross-field validation instead of the deprecated@validator.
Real-world use cases
- E-commerce API: validate product prices (must be > 0) and stock quantities (>= 0) to prevent invalid DB records.
- User registration endpoint: enforce username pattern and email format to avoid spam and bad user data.
- Analytics API: ensure page and per_page query parameters are within sane limits (1–100) to prevent DoS via huge offsets.
Key takeaways
- Type hints are not enough – you need constraints and defaults to enforce domain rules.
- Use
Fieldwith...for required fields and a default for optional ones. gt,ge,lt,le,min_length,max_length, andpatternare your bread and butter.QueryandPathgive you the same power for URL parameters.- Pydantic automatically returns 422 responses with detailed error lists – leverage that for frontend debugging.
- Document your constraints – FastAPI’s OpenAPI schema will expose them for free.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.