Lesson 5 · FastAPI deep dive
FastAPI Tutorial #5: Data Validation with Pydantic
Video five of the eighteen-part series: constraining and validating data far beyond a plain type hint. Field, Query, Path constraints, custom validators,…
- CourseFastAPI deep dive
- Lesson5 of 12
- Video17 min
- FormatJupyter notebook · 10 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbFastAPI Deep-Dive, Video 5: Data Validation Deep-Dive#
- Video five of the eighteen-part series: constraining and validating data far beyond a plain type hint.
- Field, Query, Path constraints, custom validators, and reading validation errors.
- Let's get into it.
Part 1: Field() - Constraints on Model Fields#
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class Product(BaseModel):
name: str = Field(min_length=2, max_length=50)
price: float = Field(gt=0)
@app.post('/products')
def create_product(product: Product):
return product.model_dump()
client = TestClient(app)
print(client.post('/products', json={'name': 'Mug', 'price': 9.99}).json())
Part 2: Constraint Violations#
response = client.post('/products', json={'name': 'M', 'price': -5})
print(response.status_code)
for err in response.json()['detail']:
print(err['loc'], err['msg'])
Part 3: Field with Default and Description#
class ProductV2(BaseModel):
name: str = Field(min_length=2, max_length=50)
price: float = Field(gt=0)
quantity: int = Field(default=0, ge=0, description='Units currently in stock')
schema = ProductV2.model_json_schema()
print(schema['properties']['quantity'])
Part 4: Query() with Constraints#
from fastapi import Query
@app.get('/search')
def search(q: str = Query(min_length=2, max_length=20, pattern='^[a-zA-Z0-9 ]+$')):
return {'q': q}
print(client.get('/search?q=fastapi').json())
print(client.get('/search?q=a').status_code)
print(client.get('/search?q=bad$chars').status_code)
Part 5: Path() with Constraints#
from fastapi import Path
@app.get('/products/{product_id}')
def get_product(product_id: int = Path(gt=0, le=10000)):
return {'product_id': product_id}
print(client.get('/products/42').json())
print(client.get('/products/0').status_code)
print(client.get('/products/99999').status_code)
Part 6: field_validator - Custom Validation Logic#
from pydantic import field_validator
class Username(BaseModel):
username: str
@field_validator('username')
@classmethod
def no_spaces(cls, value):
if ' ' in value:
raise ValueError('username cannot contain spaces')
return value.lower()
@app.post('/users')
def create_user(user: Username):
return user.model_dump()
print(client.post('/users', json={'username': 'Alex'}).json())
print(client.post('/users', json={'username': 'Alex Smith'}).status_code)
Part 7: model_validator - Cross-Field Validation#
from pydantic import model_validator
class SignupForm(BaseModel):
password: str
confirm_password: str
@model_validator(mode='after')
def passwords_match(self):
if self.password != self.confirm_password:
raise ValueError('passwords do not match')
return self
@app.post('/signup-form')
def signup_form(form: SignupForm):
return {'status': 'ok'}
print(client.post('/signup-form', json={'password': 'a', 'confirm_password': 'a'}).status_code)
print(client.post('/signup-form', json={'password': 'a', 'confirm_password': 'b'}).status_code)
Part 8: A Custom Email Check - No Extra Dependency Required#
class Contact(BaseModel):
name: str
email: str
@field_validator('email')
@classmethod
def email_must_look_valid(cls, value):
if '@' not in value or '.' not in value.split('@')[-1]:
raise ValueError('invalid email format')
return value
@app.post('/contacts')
def create_contact(contact: Contact):
return contact.model_dump()
print(client.post('/contacts', json={'name': 'Sam', 'email': 'sam@example.com'}).json())
print(client.post('/contacts', json={'name': 'Sam', 'email': 'not-an-email'}).status_code)
Part 9: Reading the Validation Error Format#
response = client.post('/contacts', json={'name': 'Sam', 'email': 'bad'})
error = response.json()['detail'][0]
print(sorted(error.keys()))
print(error['loc'])
print(error['type'])
Part 10: A Real Pattern - a Validated Signup Model#
class RealSignup(BaseModel):
username: str = Field(min_length=3, max_length=20)
email: str
password: str = Field(min_length=8)
confirm_password: str
@field_validator('username')
@classmethod
def username_no_spaces(cls, value):
if ' ' in value:
raise ValueError('username cannot contain spaces')
return value
@field_validator('email')
@classmethod
def signup_email_must_look_valid(cls, value):
if '@' not in value or '.' not in value.split('@')[-1]:
raise ValueError('invalid email format')
return value
@model_validator(mode='after')
def check_passwords(self):
if self.password != self.confirm_password:
raise ValueError('passwords do not match')
return self
@app.post('/real-signup', response_model=None)
def real_signup(form: RealSignup):
return {'status': 'account created', 'username': form.username}
good = {'username': 'alex', 'email': 'alex@example.com', 'password': 'hunter22', 'confirm_password': 'hunter22'}
print(client.post('/real-signup', json=good).status_code)
bad = {**good, 'confirm_password': 'different'}
print(client.post('/real-signup', json=bad).status_code)
Wrap-Up: What You Learned#
- Field adds real constraints on top of a type hint: min_length, max_length, gt, ge, and le.
- A violated Field constraint triggers the same structured 422 response, naming the specific failed rule.
- Field also accepts a default and a description, which shows up directly in the auto-generated docs.
- Query applies the same kind of constraints to query parameters, including a regex pattern.
- Path mirrors Query for path parameters, restricting a numeric id to a sensible range.
- field_validator runs custom logic on a single field; raising ValueError becomes a proper 422 error.
- model_validator(mode='after') runs after every field passes, enabling cross-field rules like matching passwords.
- Pydantic's built-in EmailStr works the same way, but needs the optional email-validator package installed first.
- Every validation error follows a consistent shape: type, loc, msg, and input, good for programmatic handling.
- That wraps up data validation. Next up: Path Operation Configuration.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



