Cheatsheet for mapping between Python field names and external API names.

Basic alias

class User(BaseModel):
    full_name: str = Field(alias="fullName")
    model_config = {"populate_by_name": True}

# Both accepted:
User.model_validate({"fullName": "Alice"})
User.model_validate({"full_name": "Alice"})

# Serialization
User(full_name="Alice").model_dump()              # {"full_name": "Alice"}
User(full_name="Alice").model_dump(by_alias=True) # {"fullName": "Alice"}

validation_alias vs serialization_alias

class User(BaseModel):
    user_id: int = Field(validation_alias="id", serialization_alias="userId")

# Input: {"id": 42}
# Output (with by_alias=True): {"userId": 42}

For asymmetric mappings.

AliasChoices (try multiple)

from pydantic import AliasChoices

class User(BaseModel):
    name: str = Field(validation_alias=AliasChoices("name", "fullName", "full_name"))

User.model_validate({"fullName": "Alice"})      # works
User.model_validate({"full_name": "Alice"})     # works
User.model_validate({"name": "Alice"})          # works

Tries each in order.

AliasPath (nested input)

from pydantic import AliasPath

class User(BaseModel):
    user_id: int = Field(validation_alias=AliasPath("data", "user", "id"))

User.model_validate({"data": {"user": {"id": 42}}})

For unwrapping nested external JSON.

AliasChoices + AliasPath

class User(BaseModel):
    user_id: int = Field(validation_alias=AliasChoices(
        AliasPath("data", "user", "id"),
        "user_id",
        "id",
    ))

populate_by_name

class User(BaseModel):
    full_name: str = Field(alias="fullName")
    model_config = {"populate_by_name": True}

Without populate_by_name: only the alias is accepted (not the Python name).

Global alias generator

from pydantic.alias_generators import to_camel

class User(BaseModel):
    full_name: str
    email_address: str
    
    model_config = {
        "alias_generator": to_camel,        # full_name → fullName
        "populate_by_name": True,
    }

User.model_validate({"fullName": "Alice", "emailAddress": "[email protected]"})

For consistent snake_case ↔ camelCase mapping.

Custom alias generator

def custom_gen(field_name: str) -> str:
    return f"x_{field_name}"

class M(BaseModel):
    name: str
    model_config = {"alias_generator": custom_gen, "populate_by_name": True}

Per-field override

class User(BaseModel):
    full_name: str = Field(alias="fullName")          # explicit
    age: int                                          # uses alias_generator
    
    model_config = {"alias_generator": to_camel, "populate_by_name": True}

Field-level alias takes precedence.

Round-trip

user = User.model_validate({"fullName": "Alice"})
data = user.model_dump(by_alias=True)
restored = User.model_validate(data)

by_alias=True outputs aliases; round-trip works.

Use cases

External API adapter

class StripeCustomer(BaseModel):
    id: str
    email: str
    created: int = Field(serialization_alias="created_at_unix")
    metadata_: dict = Field(alias="metadata")

snake_case backend ↔ camelCase frontend

class UserOut(BaseModel):
    full_name: str
    email_address: str
    
    model_config = {"alias_generator": to_camel, "populate_by_name": True}

# API returns {"fullName": "...", "emailAddress": "..."}

Reserved-word avoidance

class M(BaseModel):
    type_: str = Field(alias="type")        # 'type' is built-in; trailing underscore + alias

OpenAPI generation

Aliases reflected in JSON Schema with validation_alias. FastAPI uses for the OpenAPI spec.

Common mistakes

  • Forgetting populate_by_name=True — Python attribute access works, but model_validate by Python name fails.
  • Inconsistent by_alias in serialization across endpoints.
  • AliasGenerator on inherited models without populate_by_name — confusion.

Read this next

If you want my snake↔camel API adapter pattern, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .