Model Config and Aliases
Pydantic v2 replaced the inner class Config with model_config = ConfigDict(...). This single attribute controls validation behavior, serialization, ORM compatibility, and field population rules.
By the end of this lesson you can: use model_config with ConfigDict, define field aliases for camelCase APIs, use populate_by_name, enable ORM mode, and control serialization with model_dump.
model_config and ConfigDict
from pydantic import BaseModel, ConfigDict
class AppModel(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True, # strip leading/trailing spaces from str fields
str_to_lower=False,
use_enum_values=True, # store enum.value not enum instance
validate_default=True, # also validate default values
populate_by_name=True, # allow populating by field name even if alias set
from_attributes=True, # ORM mode — read from object attributes
frozen=False, # allow mutation after creation
extra="ignore", # silently ignore unknown fields
)
Common options:
| Option | Default | Effect |
|---|---|---|
str_strip_whitespace | False | Strip whitespace from all string inputs |
use_enum_values | False | Store .value not enum instance |
populate_by_name | False | Allow field name in addition to alias |
from_attributes | False | Enable ORM mode (read attrs, not dict keys) |
extra | "ignore" | "ignore", "allow", or "forbid" extra fields |
frozen | False | Make instances immutable |
Field Aliases
Use alias when the JSON key name differs from the Python attribute name (e.g., camelCase APIs):
from pydantic import BaseModel, Field, ConfigDict
class WebhookPayload(BaseModel):
model_config = ConfigDict(populate_by_name=True)
event_type: str = Field(..., alias="eventType")
user_id: int = Field(..., alias="userId")
created_at: str = Field(..., alias="createdAt")
With populate_by_name=True, both eventType (alias) and event_type (name) are accepted as input.
Serialization Aliases
Separate the input alias from the output alias with serialization_alias:
from pydantic import BaseModel, Field, ConfigDict
class ItemResponse(BaseModel):
model_config = ConfigDict(populate_by_name=True)
item_id: int = Field(..., serialization_alias="itemId")
display_name: str = Field(..., serialization_alias="displayName")
unit_price: float = Field(..., serialization_alias="unitPrice")
item = ItemResponse(item_id=1, display_name="Widget", unit_price=9.99)
print(item.model_dump(by_alias=True))
# {"itemId": 1, "displayName": "Widget", "unitPrice": 9.99}
print(item.model_dump())
# {"item_id": 1, "display_name": "Widget", "unit_price": 9.99}
ORM Mode (from_attributes=True)
Enable ORM mode to construct Pydantic models directly from SQLAlchemy ORM objects:
from pydantic import BaseModel, ConfigDict
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
email: str
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import UserORM
from app.models.user import UserResponse
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
user_orm = await db.get(UserORM, user_id)
# With from_attributes=True, Pydantic reads ORM object attributes directly
return UserResponse.model_validate(user_orm)
Without from_attributes=True, model_validate() only accepts dicts. With it, it accepts any object with matching attributes — including SQLAlchemy ORM instances.
Controlling Serialization Output
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
username: str
password: str = Field(..., exclude=True) # Never serialized
email: str
class UserResponse(BaseModel):
id: int
username: str
email: str
user = UserCreate(username="alice", password="secret", email="a@example.com")
print(user.model_dump())
# {"username": "alice", "email": "alice@example.com"} — password excluded
You can also exclude/include at call time:
user = UserResponse(id=1, username="alice", email="alice@example.com")
print(user.model_dump(exclude={"email"}))
# {"id": 1, "username": "alice"}
print(user.model_dump(include={"id", "username"}))
# {"id": 1, "username": "alice"}
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
class Config silently ignored | Pydantic v2 does not use inner Config class | Replace with model_config = ConfigDict(...) |
| Alias not working for input | populate_by_name=False and using field name | Set populate_by_name=True or always use alias in input |
| ORM object returns empty model | from_attributes=False | Add model_config = ConfigDict(from_attributes=True) |
by_alias=True not propagating nested | .model_dump(by_alias=True) not set | Pass by_alias=True explicitly at each serialization call |
| Extra fields cause validation error | extra="forbid" set | Change to extra="ignore" or handle the extra fields |
Hands-On Practice
from pydantic import BaseModel, Field, ConfigDict
class PaymentCreate(BaseModel):
"""Accepts camelCase from frontend clients"""
model_config = ConfigDict(populate_by_name=True)
card_number: str = Field(..., alias="cardNumber", min_length=16, max_length=16)
card_holder: str = Field(..., alias="cardHolder", min_length=2)
expiry_month: int = Field(..., alias="expiryMonth", ge=1, le=12)
expiry_year: int = Field(..., alias="expiryYear", ge=2024, le=2040)
cvv: str = Field(..., alias="cvv", min_length=3, max_length=4, exclude=True)
class PaymentResponse(BaseModel):
"""Returns snake_case — card number is masked"""
model_config = ConfigDict(populate_by_name=True)
transaction_id: str
masked_card: str
card_holder: str
status: str
from app.models.payment import PaymentCreate
# Accept camelCase input
payload = {
"cardNumber": "4111111111111111",
"cardHolder": "Jane Doe",
"expiryMonth": 12,
"expiryYear": 2027,
"cvv": "123",
}
payment = PaymentCreate.model_validate(payload)
print(payment.card_holder) # Jane Doe
print(payment.model_dump()) # cvv excluded, snake_case output