When the wire format and your Python names disagree - camelCase, reserved words, and fields that arrive under more than one name.
Overview
Four reasons a field needs another name
The name a field has in your code is not always the name it has in the data. An external API may send camelCase keys, while your Python code uses snake_case. A legacy system may use cryptic names you would rather not spread through your code. An alias connects the two.
An alias tells Pydantic that the input key differs from the attribute name. You work with a clean name in Python and accept the awkward name on the wire. That keeps outside conventions at the edge, where they belong, instead of leaking into your own code.
This article begins with the four reasons a field needs another name. It then shows the simple form of an alias and how to use by_alias on the way out. It covers how to separate the direction, so the name you accept can differ from the name you send. You will also see how a field can accept several spellings, which helps when you migrate from an old format. Aliases are a small feature, but a lot of real integration work depends on them.
Anyone who has connected to a third-party service has met this problem. The data never quite matches the naming you would choose. You can rename things by hand in a conversion step, but that step is one more place for a typo to hide. An alias moves the mapping into the model, where it is declared once and checked every time. The same declaration can serve both directions, or each direction can have its own rule, depending on what the other side expects. The article also touches on the settings that decide whether the alias or the real name is accepted on input, which is a frequent source of confusion. Learn it once, and mismatched names stop being a source of friction in your integrations.
The wire is camelCase. JavaScript clients and a great many APIs use publishedAt. Python uses published_at. Renaming your fields to match makes each Python file read badly. Renaming their payload isn't an option.
The key is a reserved word. A payload with from, class, import or id can't map onto an field with the same name.
The name is bad. A third-party API calls a thing dt2. You don't have to.
The name changed. You're renaming a field and both spellings must work through a deprecation window.
Worth knowing
Field(alias=...) sets one name used for both input and output. By default it replaces the Python name as input.
populate_by_name=True lets a field be filled by either its alias or its Python name — almost always what you want.
validation_alias and serialization_alias control the two directions independently.
model_dump(by_alias=True) is required to emit aliases. Without it you get Python names, which is a common cause of “my API returns snake_case”.
AliasChoices accepts several input spellings; AliasPath pulls a value out of a nested payload.
alias_generator on a shared base applies a naming rule to every field, so a whole API can be camelCase in one line.
Aliases: When the Wire and Your Code Disagree
camelCase payloads, reserved words, legacy names, and nested shapes flattened into yours.
The wire says camelCase
An alias lets a field be populated from a different key without renaming your Python attribute.
example_01.pyPydantic
from pydantic import BaseModel, Field
class Module(BaseModel):
title: str
published_at: str = Field(alias="publishedAt")
reading_minutes: int = Field(alias="readingMinutes")
payload = {"title": "Vectors", "publishedAt": "2026-08-26", "readingMinutes": 8}
m = Module.model_validate(payload)
print("python side :", m.published_at, "|", m.reading_minutes)
print("dump :", m.model_dump())
print("dump by alias:", m.model_dump(by_alias=True))
Output
By default the alias replaces the name
Once a field has an alias, the Python name no longer works as input — unless you turn populate_by_name on.
example_02.pyPydantic
from pydantic import BaseModel, ConfigDict, Field, ValidationError
class Strict(BaseModel):
published_at: str = Field(alias="publishedAt")
class Either(BaseModel):
model_config = ConfigDict(populate_by_name=True)
published_at: str = Field(alias="publishedAt")
print("alias works :", Strict(publishedAt="2026-08-26"))
try:
Strict(published_at="2026-08-26")
except ValidationError as e:
print("name refused :", e.errors()[0]["type"], e.errors()[0]["loc"])
print()
print("with populate_by_name, both work:")
print(" ", Either(publishedAt="2026-08-26"))
print(" ", Either(published_at="2026-08-26"))
Output
Different names in and out
validation_alias and serialization_alias separate the two directions, which a single alias cannot.
example_03.pyPydantic
from pydantic import BaseModel, Field
class Module(BaseModel):
minutes: int = Field(validation_alias="durationMinutes",
serialization_alias="reading_minutes")
m = Module.model_validate({"durationMinutes": 8})
print("python :", m.minutes)
print("plain dump:", m.model_dump())
print("by alias :", m.model_dump(by_alias=True))
print()
print("It reads one name and writes another - useful when translating")
print("between two systems that disagree about both.")
Output
Accepting several spellings
AliasChoices takes the first key that is present, which is how you support an old and a new name at once.
example_04.pyPydantic
from pydantic import AliasChoices, BaseModel, Field
class Module(BaseModel):
minutes: int = Field(
validation_alias=AliasChoices("minutes", "readingMinutes", "duration"))
for payload in [{"minutes": 8},
{"readingMinutes": 9},
{"duration": 10}]:
print("%-26s -> %d" % (str(payload), Module.model_validate(payload).minutes))
print()
print("One field, three accepted spellings, no branching in your code.")
Output
Reaching into a nested payload
AliasPath pulls a value out of a nested structure, flattening someone else's shape into yours.
example_05.pyPydantic
from pydantic import AliasPath, BaseModel, Field
class Module(BaseModel):
title: str
author_name: str = Field(validation_alias=AliasPath("author", "name"))
first_tag: str = Field(validation_alias=AliasPath("tags", 0))
payload = {
"title": "Vectors",
"author": {"name": "Ada", "email": "ada@vizlearn.in"},
"tags": ["maths", "linear-algebra"],
}
m = Module.model_validate(payload)
print(m)
print()
print("A nested API response became a flat model, with no manual digging.")
Output
Generating aliases for the whole model
alias_generator applies a rule to every field, which beats writing alias= forty times.
example_06.pyPydantic
from pydantic import BaseModel, ConfigDict
def to_camel(name: str) -> str:
head, *rest = name.split("_")
return head + "".join(word.capitalize() for word in rest)
class ApiModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel,
populate_by_name=True)
class Module(ApiModel):
title: str
published_at: str
reading_minutes: int
m = Module.model_validate({"title": "Vectors",
"publishedAt": "2026-08-26",
"readingMinutes": 8})
print("python :", m.reading_minutes)
print("out :", m.model_dump(by_alias=True))
print("by name:", Module(title="X", published_at="Y", reading_minutes=1).reading_minutes)
Output
The simple form
published_at: str = Field(alias="publishedAt")
One alias, used for input and output. There's one action here that catches all: by default the alias replaces the Python name as input. Module(published_at="...") now raises. This Is surprising when you've been making the model in your own tests.
populate_by_name=True fixes it:
model_config = ConfigDict(populate_by_name=True)
Now either spelling works on the way in. This is almost always what you want. It's worth setting on a base model so no one has to rediscover it.
Losing this is the most common alias bug. The symptom is "my API accepts camelCase but returns snake_case". The aliases were configured right. The output call didn't ask for them.
In FastAPI, response_model_by_alias defaults to True. Responses use aliases on its own. Which means a manual model_dump() elsewhere in the same code behaves differently from the endpoint. Worth knowing before you spend an afternoon on it.
Separating the directions
A single alias uses the same name both ways. When the directions differ, use the two exact settings:
minutes: int = Field(validation_alias="durationMinutes",
serialization_alias="reading_minutes")
Read one name, write another. That sounds exotic and comes up when you sit between two systems that disagree. Consuming a partner API and exposing your own shape, or migrating a field where you must accept the old name and emit the new one.
Where both are set, they win over a plain alias in their own directions.
Several accepted spellings
AliasChoices takes the first key present:
minutes: int = Field(validation_alias=AliasChoices("minutes", "readingMinutes", "duration"))
This is the clean way to run a rename. Add the new name to the front, keep the old one, ship. Clients migrate at their own pace. Neither your model nor your handlers need a branch for it. Drop the old entry when the traffic stops.
It also handles the plain mess of a payload assembled by a few teams over a few years. Here The same value appears under three names based on which service made it.
Reaching into nested data
AliasPath pulls a value out of a nested structure:
Strings are keys, ints are indices. A deeply nested third-party response turns a flat model with no manual digging and no mid models you didn't want.
Use it with judgement. Flattening two or three values from a response is just right. Flattening twenty makes a model whose link to the payload is hopeless to see. Modelling the real structure with nested models is clearer.
Note that AliasPath is check-only. There's no output equal. Rebuilding a nested shape from flat fields isn't a rename. If you need that, a custom serialiser or a split output model is the answer.
Doing it for the whole model
Writing alias="..." on forty fields is tedious and drifts. alias_generator applies a rule to each field:
class ApiModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
Inherit from that and each model in your API speaks camelCase, with no per-field config at all. Pydantic ships to_camel and to_pascal in pydantic.alias_generators. You often don't even write the function.
An clear alias on a field replaces the gen. This Is what you want for the handful of exceptions each real API has.
This is the right shape for a code-wide rule. One base model, one setting, each model steady. One place to change it.
Aliases in the schema
The made JSON Schema uses aliases. This Is correct. The schema shows the wire format. The wire format is what the alias names.
So consumers of your OpenAPI doc see publishedAt, made clients give publishedAt. The docs matches what the endpoint really accepts. This is one of the places where getting aliases right pays off beyond your own code.
Things that go wrong
**Losing by_alias=True** on a manual dump. The most common one.
**Losing populate_by_name** and then being unable to make the model in tests using Python names.
Aliasing to a name that collides with another field's name. This Makes confusing action rather than a clear error.
Using aliases to paper over a bad model. If half your fields need aliases to make sense, the model may be showing someone else's payload rather than your domain. A translation layer. An input model matching their shape, converted to yours. Is at times clearer than twenty aliases.
Aliases and nested models
An alias applies to the field it's declared on. Nesting is unaffected. A nested model's own aliases apply when it's checked. by_alias=True passes through the whole tree on the way out.
So a camelCase rule applied through a shared base model reaches each level without extra work. This Is the main reason to prefer alias_generator on a base over per-field aliases.
The one thing to check is agreement. A tree where the parent is aliased and one nested model isn't makes output that is camelCase at the top and snake_case two levels down. Valid, confusing. Just the kind of thing that lives review.
What the schema says
made schemas use aliases. This Is correct: the schema shows the wire. The alias is the wire name.
That has a hands-on result worth knowing. If you make a schema for inner purposes and expect Python names, you won't get them. model_json_schema(by_alias=False) makes the Python-named version when that is truly what you need.
For anything published, the aliased schema is the right one. It matches what the endpoint accepts and returns.
Aliases in error messages
An error's loc uses the alias, not the Python name, when check failed on an aliased field.
That is the right action. The caller sent publishedAt and should be told about publishedAt, not about an field name they've never seen. But it means error-handling code can't assume loc matches your field names. This At times matters when mapping errors back to inner state.
A migration recipe
Renaming a field in a live API, without breaking anyone:
Step one. Add the new name as the main, keep the old one accepted:
minutes: int = Field(validation_alias=AliasChoices("minutes", "readingMinutes"))
Both work on the way in. Output uses whichever you set as the output alias. Keep emitting the old name for now.
Step two. Switch the output alias to the new name. Clients reading the response see the new one. Clients sending either still work.
Step three. Once traffic on the old name stops, drop it from AliasChoices.
Three small deploys, no coordinated cutover, no broken clients. This is the pattern aliases make possible and it's worth knowing before you need it.
Deciding whether you need them at all
A model full of aliases is at times a sign that the model is showing someone else's payload rather than your domain.
If you consume a third-party API with thirty oddly-named fields, two models can be cleaner than thirty aliases. One matching their shape just, with their names. A conversion into yours. The first model docs their API honestly. The second is your domain, unpolluted.
Aliases are best when the gap is a *rule*. CamelCase versus snake_case, a reserved word, a rename in progress. When the gap is a whole foreign vocab, a translation layer says more.
Aliases: summary
alias for one name both ways. validation_alias and serialization_alias when the directions differ. populate_by_name=True so Python names still work as input. by_alias=True to emit them.
AliasChoices for a few accepted spellings. This Makes renames painless. AliasPath for flattening nested payloads. alias_generator on a shared base for a whole-API rule.
And the one to note: aliases aren't used on output unless you ask for them.
The one-line summary
Aliases exist so your Python can read like Python while your API reads like what your consumers expect.
Set the rule once on a shared base with alias_generator and populate_by_name, note by_alias=True when dumping by hand. Reach for AliasChoices the moment you need to rename a thing without breaking anyone.
all else in this module is a variation on those three.
Mistakes people make with Aliases
**Losing by_alias=True.** Comfortably the most common. Aliases aren't used on output unless requested. The symptom. An API accepting camelCase and returning snake_case. Looks like a config problem rather than a missing arg on one call.
**Losing populate_by_name.** An alias replaces the Python name as input. Your own tests can no longer make the model with the names you wrote. The fix is one config entry and it belongs on a shared base.
Inconsistent nesting. A parent with an alias gen and a nested model without one makes output that is camelCase at the top and snake_case two levels down. Valid, confusing. Just the sort of thing that lives review.
**given loc matches your field names.** Errors report the alias. That is the name the caller used. Code mapping errors back to inner state has to account for it.
**Expecting AliasPath to work on the way out.** It's check-only. Rebuilding a nested shape from flat fields isn't a rename. Needs a serialiser or a split output model.
Aliasing an entire foreign vocab. Thirty aliases often means the model is showing someone else's payload rather than your domain. Two models. One matching their shape, one matching yours, with a conversion between. Says more and stays readable.
The failure mode to expect
If a thing about aliases isn't working, check by_alias=True first.
It accounts for most alias problems, it makes no error. The symptom. An API that accepts one rule and returns another. Looks like a config problem rather than a missing arg on a single call.
The second thing to check is populate_by_name. This Is what stops your own tests being able to make the model using the names you wrote.
The convention decision
Behind the mechanics is one choice worth making on purpose rather than per model.
Does your API speak the wire's rule, or your language's? Both are defensible. Agreement is what matters. A mixed API is worse than either choice made badly.
If your consumers are browsers and JavaScript clients, camelCase on the wire is what they expect. An alias_generator on a shared base gives it to each model at once with no per-field work.
If your consumers are Python services, snake_case throughout is simpler and needs no aliases at all.
What makes trouble is deciding case by case. Some endpoints camelCase, some not, some models aliased and their nested models not. each consumer then needs to know which is which. No amount of docs makes that pleasant.
Make the choice once, express it in a base model. Let the exceptions be genuine exceptions rather than accidents.
Questions people ask about Aliases
Why does my model reject the Python field name after I add an alias? By default an alias replaces the field name as the input key, so Module(published_at=...) raises. Set populate_by_name=True in model_config and both spellings work.
Why does model_dump() return snake_case when I set an alias? Aliases are only used on output when you ask for them. Call model_dump(by_alias=True), or set serialize_by_alias in the config on newer releases.
What is the difference between alias, validation_alias and serialization_alias?alias sets one name for both directions. validation_alias only controls the input key and serialization_alias only controls the output key, and each wins over alias in its own direction.
How do I accept several spellings of the same key? Use validation_alias=AliasChoices("durationMinutes", "duration", "minutes"). Pydantic takes the first key it finds in the input.
Can I make every field camelCase without writing each alias? Yes. Put alias_generator=to_camel (from pydantic.alias_generators) in the config of a shared base model, and every field gets a camelCase alias.
How do I read a value from inside a nested payload? Use AliasPath("author", "name") as the validation alias. It walks the keys in order and pulls the inner value up into a flat field.
One thing to remember
If just one fact lives this module, make it by_alias=True.
Aliases configured just right still do nothing on output until that arg is passed, the failure is silent. The symptom looks like a config problem rather than a missing keyword on a single call. It's the first thing to check when aliases appear not to work.
Check yourself
0 of 4
Answer without scrolling back up.
You set `alias="publishedAt"`. Why does `Module(published_at=...)` now fail?
The alias becomes the input name. `populate_by_name=True` restores the Python name as an accepted alternative, which is almost always what you want.
Your API accepts camelCase but returns snake_case. What is missing?
Aliases are not used for output unless requested. FastAPI does this for you on response models, which is why a manual dump elsewhere can behave differently.
What is `AliasChoices` for?
It is the clean way to run a rename: add the new name, keep the old, and clients migrate at their own pace with no branching in your code.
What does `AliasPath("author", "name")` do?
Strings are keys and integers are indices, so a nested response flattens into your model. It is validation-only - there is no serialisation equivalent.
Cheat sheet
Aliases
Map camelCase payloads, reserved Python names and legacy fields to clean attribute names using aliases and AliasPath.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.