BaseModel is Pydantic's foundation class. Subclasses declare fields as
class-level annotations; Pydantic validates and coerces incoming data at
instantiation time.
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
quantity: int = 1 # default value
item = Item(name="Widget", price=9.99)
# Item(name='Widget', price=9.99, quantity=1)
Item(name="Widget", price="nine") # raises ValidationError: price must be float
FastAPI uses BaseModel to:
- Parse and validate incoming JSON bodies.
- Serialise outgoing responses.
- Generate OpenAPI JSON Schema.
Rule of thumb: use a BaseModel subclass for any structured data that crosses
the HTTP boundary — it's free validation and documentation.
Use Field() from Pydantic:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100, description="Product name")
price: float = Field(gt=0, description="Price in USD", example=9.99)
sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$", examples=["ABC-1234"])
Field() parameters:
- Constraints:
gt,ge,lt,le,min_length,max_length,pattern,multiple_of - Metadata:
title,description,example,examples - Behaviour:
default,default_factory,alias,exclude
Rule of thumb: use Field() for any field that needs a constraint or a helpful
description — it ends up in the OpenAPI schema and saves clients guessing.
- Required: no default value — Pydantic raises
ValidationErrorif omitted. - Optional with None:
field: T | None = None— acceptsNoneor the type. - Optional with default:
field: T = default_value.
from pydantic import BaseModel
class User(BaseModel):
id: int # required
name: str # required
bio: str | None = None # optional, defaults to None
role: str = "viewer" # optional, defaults to "viewer"
In OpenAPI:
- Required fields →
required: [...]array in the schema. - Optional fields → absent from
required, withdefaultornullable.
Rule of thumb: think of None default as "not provided"; use a semantic default
like "viewer" when the field has a meaningful fallback value.
An alias lets the JSON key differ from the Python attribute name. Useful when the API uses camelCase, hyphens, or reserved Python keywords.
from pydantic import BaseModel, Field
class Order(BaseModel):
order_id: int = Field(alias="orderId") # JSON: "orderId"
item_count: int = Field(alias="itemCount")
# Parsing from JSON (camelCase input)
order = Order.model_validate({"orderId": 42, "itemCount": 3})
print(order.order_id) # 42 (Python snake_case attribute)
For global camelCase ↔ snake_case conversion use model_config:
from pydantic import ConfigDict
class MyModel(BaseModel):
model_config = ConfigDict(alias_generator=lambda s: s.replace("_", ""), populate_by_name=True)
order_id: int
Rule of thumb: use alias for a few fields; use alias_generator when the
entire API uses camelCase (common with JavaScript clients).
A child model inherits all fields from its parent and can add or override them.
class ItemBase(BaseModel):
name: str
price: float
class ItemCreate(ItemBase):
# used for POST body — no id yet
pass
class ItemUpdate(ItemBase):
# all fields optional for PATCH
name: str | None = None
price: float | None = None
class ItemOut(ItemBase):
id: int # added by DB
created_at: datetime
This pattern keeps field definitions DRY while giving each use-case the exact shape it needs.
Rule of thumb: define a Base model with shared fields, then derive Create,
Update, and Out variants — the "Input/Output DTO" pattern.
model_config = ConfigDict(...) replaces Pydantic v1's inner class Config.
Key options:
| Setting | Effect |
|---|---|
extra="forbid" |
Reject extra fields |
frozen=True |
Make instances immutable (hashable) |
populate_by_name=True |
Allow both alias and name |
from_attributes=True |
Parse from ORM objects (SQLAlchemy) |
str_strip_whitespace=True |
Auto-strip leading/trailing spaces |
alias_generator=fn |
Auto-generate aliases for all fields |
from pydantic import BaseModel, ConfigDict
class StrictUser(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
id: int
name: str
Rule of thumb: set from_attributes=True on any model that reads from SQLAlchemy
ORM objects; set extra="forbid" on request models to catch client typos.
By default Pydantic models only parse from dicts. from_attributes=True allows
parsing from any object with attributes — including SQLAlchemy ORM instances.
from pydantic import BaseModel, ConfigDict
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase): pass
class UserORM(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
user_orm = session.get(UserORM, 1)
user_out = UserOut.model_validate(user_orm) # reads .id and .name attributes
Without from_attributes=True, model_validate(orm_obj) raises a
ValidationError because ORM objects aren't dicts.
Rule of thumb: always set from_attributes=True on output schemas that will
be constructed from SQLAlchemy models.
Use @computed_field (Pydantic v2):
from pydantic import BaseModel, computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
r = Rectangle(width=3.0, height=4.0)
print(r.area) # 12.0
print(r.model_dump()) # {"width": 3.0, "height": 4.0, "area": 12.0}
@computed_field fields are included in serialisation and the OpenAPI schema.
They are always read-only — you can't set them from input.
Rule of thumb: use @computed_field for derived values that belong in the
response (full name from first + last, URL from ID); avoid heavy computation in them.
Model.model_validate(obj) is the explicit way to parse data in Pydantic v2.
It accepts a dict or any object (with from_attributes=True) and returns a
validated model instance.
# constructor — same as model_validate for dicts
item = Item(name="Widget", price=9.99)
# model_validate — more explicit, required for non-dict input
item = Item.model_validate({"name": "Widget", "price": 9.99})
item = Item.model_validate(orm_instance) # needs from_attributes=True
In FastAPI, model_validate is called internally when parsing request bodies.
You call it explicitly when converting ORM objects to Pydantic models in service
or repository layers.
Rule of thumb: use the constructor for tests with literal dicts; use
model_validate in production code where the source object might be an ORM row.
model_dump() returns a Python dict of the model's fields. It's Pydantic v2's
replacement for .dict().
item = Item(name="Widget", price=9.99)
item.model_dump()
# {"name": "Widget", "price": 9.99}
# exclude fields
item.model_dump(exclude={"price"})
# {"name": "Widget"}
# only unset fields (for PATCH)
item.model_dump(exclude_unset=True)
# JSON-safe output (datetime → str, UUID → str)
item.model_dump(mode="json")
Rule of thumb: use model_dump(exclude_unset=True) in PATCH handlers to get
only the fields the client explicitly sent.
Call Model.model_json_schema():
import json
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(min_length=1)
price: float = Field(gt=0)
print(json.dumps(Item.model_json_schema(), indent=2))
# {
# "title": "Item",
# "type": "object",
# "properties": {
# "name": {"type": "string", "minLength": 1},
# "price": {"type": "number", "exclusiveMinimum": 0}
# },
# "required": ["name", "price"]
# }
FastAPI embeds this schema in /openapi.json automatically. You might call
model_json_schema() directly to validate schemas in tests or export them to
other systems.
Rule of thumb: write a test that calls model_json_schema() and asserts key
properties — it catches breaking schema changes before they hit production.
| Feature | Pydantic v1 | Pydantic v2 |
|---|---|---|
| Config | class Config: |
model_config = ConfigDict(...) |
| ORM mode | orm_mode = True |
from_attributes=True |
| Serialise | .dict() / .json() |
.model_dump() / .model_dump_json() |
| Parse | MyModel(**data) / .parse_obj() |
.model_validate(data) |
| Validators | @validator |
@field_validator / @model_validator |
| Performance | Pure Python | Rust core (10-50× faster) |
FastAPI 0.100+ requires Pydantic v2. Code targeting both versions uses the
pydantic.v1 compatibility shim.
Rule of thumb: always use Pydantic v2 APIs in new code; if maintaining a v1 codebase, migrate validators first (they have the most breaking changes).
A discriminated union uses a Literal field as a type tag to unambiguously
select which model to use during parsing, instead of trying each model in order.
from typing import Literal, Union, Annotated
from pydantic import BaseModel, Field
class Cat(BaseModel):
type: Literal["cat"]
meows: bool
class Dog(BaseModel):
type: Literal["dog"]
barks: bool
class PetPayload(BaseModel):
pet: Annotated[Union[Cat, Dog], Field(discriminator="type")]
payload = PetPayload.model_validate({"pet": {"type": "dog", "barks": True}})
print(type(payload.pet)) # <class 'Dog'>
Discriminated unions are:
- Faster — no trial-and-error parsing.
- Clearer errors — "expected type to be 'cat' or 'dog'" vs generic failure.
- Better OpenAPI — generates
oneOfwith a discriminator property.
Rule of thumb: whenever a body can be one of several shapes, add a type tag
and use a discriminated union — it's explicit, fast, and self-documenting.
More Pydantic & Validation interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.