Skip to main content

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.

Learning Focus

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

app/models/base.py
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:

OptionDefaultEffect
str_strip_whitespaceFalseStrip whitespace from all string inputs
use_enum_valuesFalseStore .value not enum instance
populate_by_nameFalseAllow field name in addition to alias
from_attributesFalseEnable ORM mode (read attrs, not dict keys)
extra"ignore""ignore", "allow", or "forbid" extra fields
frozenFalseMake instances immutable

Field Aliases

Use alias when the JSON key name differs from the Python attribute name (e.g., camelCase APIs):

app/models/external.py
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:

app/models/api.py
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:

app/models/user.py
from pydantic import BaseModel, ConfigDict

class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: int
username: str
email: str
app/routers/users.py
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)
note

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

app/models/user.py
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

PitfallCause / SymptomFix
class Config silently ignoredPydantic v2 does not use inner Config classReplace with model_config = ConfigDict(...)
Alias not working for inputpopulate_by_name=False and using field nameSet populate_by_name=True or always use alias in input
ORM object returns empty modelfrom_attributes=FalseAdd model_config = ConfigDict(from_attributes=True)
by_alias=True not propagating nested.model_dump(by_alias=True) not setPass by_alias=True explicitly at each serialization call
Extra fields cause validation errorextra="forbid" setChange to extra="ignore" or handle the extra fields

Hands-On Practice

app/models/payment.py
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
test_payment_models.py
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

What's Next