Every FastAPI endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. you ask AI to generate starts with a Pydantic model. The model defines what data comes in, what data goes out, and what gets rejected. If you understand Pydantic, you can read any FastAPI endpoint AI writes and spot the gaps. If you don't, you ship endpoints that accept garbage data and store it in your database.
Pydantic is the most important dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. in the FastAPI ecosystem. It is not a nice-to-have, it is the reason FastAPI can auto-generate docs, validate request bodies, and serialize responses. Everything flows through Pydantic.
What is a Pydantic model?
A Pydantic model is a Python class that inherits from BaseModel. Each attribute has a type annotationWhat is type annotation?Explicitly labeling a variable or function parameter with its type in TypeScript (e.g., name: string)., and Pydantic enforces those types at runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary..
from pydantic import BaseModel
class User(BaseModel):
name: str
email: str
age: int
is_active: bool = TrueThis simple class does more than you might expect:
| What you get | How it works |
|---|---|
| Type validation | Send age: "hello" and Pydantic raises a ValidationError |
| Type coercion | Send age: "25" (string) and Pydantic converts it to 25 (int) |
| Default values | is_active defaults to True if not provided |
| JSON Schema | Pydantic generates a JSON Schema automatically, FastAPI uses this for docs |
| Serialization | Call .model_dump() to get a plain dict, .model_dump_json() for a JSON string |
When FastAPI receives a POST request with a JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. body, it passes that JSON to your Pydantic model. If validation fails, FastAPI returns a 422 error with a detailed error message, you don't write any of that error handling yourself.
Required vs optional fields
This is where beginners get confused, and where AI gets it wrong about half the time.
from pydantic import BaseModel
from typing import Optional
class UserCreate(BaseModel):
# Required - must be in the request body
name: str
email: str
# Optional - can be omitted from the request (defaults to None)
bio: Optional[str] = None
# Has a default value - can be omitted (defaults to "user")
role: str = "user"The rules are straightforward:
- No default value = required. If the client doesn't send it, Pydantic rejects the request.
= Noneor= some_value= optional. The client can skip it.Optional[str]without= None= the field is still required, but it can beNone. This trips up everyone.
# This field is REQUIRED but can be None
middle_name: Optional[str]
# This field is OPTIONAL and defaults to None
middle_name: Optional[str] = NoneOptional[str] when it means "the user doesn't have to send this field." But without = None, the field is still required, the client must explicitly send null. Always check that optional fields have a default value.Type coercionWhat is type coercion?JavaScript automatically converting a value from one type to another during an operation, like turning a string into a number for subtraction. and strict mode
Pydantic tries to be helpful by converting compatible types. Send "42" for an int field, and Pydantic silently converts it to 42. Send "true" for a bool field, and you get True.
This is usually convenient, but sometimes dangerous:
class Config(BaseModel):
max_retries: int
debug: bool
# All of these work - Pydantic coerces the types
Config(max_retries="3", debug="yes") # max_retries=3, debug=True
Config(max_retries=3.9, debug=1) # max_retries=3, debug=TrueIf you want strict validation (no coercion), you can enable strict mode:
from pydantic import BaseModel, ConfigDict
class StrictConfig(BaseModel):
model_config = ConfigDict(strict=True)
max_retries: int
debug: bool
# Now "3" for an int field raises a ValidationError
StrictConfig(max_retries="3", debug=True) # ValidationError!Most of the time, the default coercion behavior is fine. But know it exists, it explains why your endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. accepts data you didn't expect.
Creating models from data
You will see three ways to create Pydantic models from external data. AI uses them interchangeably, but they serve different purposes.
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: float
in_stock: bool = True
# 1. Direct instantiation - from keyword arguments
product = Product(name="Widget", price=9.99)
# 2. model_validate() - from a dict (replaces the old parse_obj)
data = {"name": "Widget", "price": 9.99}
product = Product.model_validate(data)
# 3. model_validate_json() - from a JSON string (replaces parse_raw)
json_string = '{"name": "Widget", "price": 9.99}'
product = Product.model_validate_json(json_string)| Method | Input | When to use |
|---|---|---|
Product(...) | Keyword args | When you're constructing in code |
model_validate(dict) | Python dict | When parsing database rows, form data, or dicts from other libs |
model_validate_json(str) | JSON string | When parsing raw JSON from a file, message queue, or webhook |
parse_obj(), parse_raw(), .dict(), .json(). These still work but are deprecated in Pydantic v2. The modern equivalents are model_validate(), model_validate_json(), model_dump(), and model_dump_json(). If you see the old names, the AI is working from outdated training data.Validation errors are detailed
When validation fails, Pydantic doesn't just say "bad data." It tells you exactly which fields failed and why.
from pydantic import ValidationError
class User(BaseModel):
name: str
age: int
email: str
try:
user = User(name=123, age="not a number")
except ValidationError as e:
print(e.errors())Output:
[
{
'type': 'string_type',
'loc': ('name',),
'msg': 'Input should be a valid string',
'input': 123
},
{
'type': 'int_parsing',
'loc': ('age',),
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'input': 'not a number'
},
{
'type': 'missing',
'loc': ('email',),
'msg': 'Field required',
'input': {'name': 123, 'age': 'not a number'}
}
]FastAPI transforms these errors into a structured 422 JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. response automatically. You never write error formatting code, Pydantic and FastAPI handle it.
How FastAPI uses Pydantic models
In FastAPI, you declare a Pydantic model as a parameter type, and FastAPI does the restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources.:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ItemCreate(BaseModel):
name: str
price: float
description: str = ""
@app.post("/items")
async def create_item(item: ItemCreate):
# 'item' is already validated - if we reach this line,
# the data is guaranteed to match the model
return {"name": item.name, "price": item.price}What happens behind the scenes when a request hits /items:
- FastAPI reads the raw JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. body
- It passes the JSON to
ItemCreate.model_validate() - If validation fails, FastAPI returns a 422 response, your function never runs
- If validation passes, your function receives a fully typed
ItemCreateinstance
This is the core promiseWhat is promise?An object that represents a value you don't have yet but will get in the future, letting your code keep running while it waits. of FastAPI: by the time your code runs, the data is valid. But "valid" only means "matches the types and constraints you defined." If you defined price: float without any constraints, then -999.99 is a perfectly valid price. That is the gap you need to learn to spot.