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​