Typorithm · medium

Aliases accept legacy names without widening writable input

pythonpydanticindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Aliases accept legacy names without widening writable input

0%

Concept and contract

Separate validation names, internal names and response names, and reject unexpected siblings.

An API migration may need to accept a legacy external name while keeping a stable internal field and a different public response name. Validation aliases decide which input names are understood; serialization aliases decide which names can be emitted. Neither is the same as allowing arbitrary extra fields.

The Identity model stores display_name internally. It accepts displayName as an external name and permits the internal spelling through validate_by_name. A by-alias dump emits name. This creates a deliberate three-name mapping instead of relying on a client to infer the internal Python attribute from an output key.

Mechanism and worked example

The example validates JSON using explicit configuration and then dumps with by_alias=True.

The example validates JSON using explicit configuration and then dumps with by_alias=True. A client can submit displayName or the permitted internal spelling and receives the same public name field. An unexpected admin key fails extra="forbid" even though the known display name itself is valid.

Current Pydantic 2.14 patch releases tightened handling of alias-related extra input, including security fixes in 2.14.2. The local lab uses 2.14.3 and tests the actual configured behavior. Conflicting submitted aliases should not be treated as a trustworthy way to select an internal value or as a reason to silently widen the writable shape.

Complete worked example

python

import json
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError

class Identity(BaseModel):
    model_config = ConfigDict(extra="forbid", validate_by_alias=True,
                              validate_by_name=True)
    display_name: str = Field(validation_alias="displayName",
                              serialization_alias="name")

def inspect_example(payload):
    try:
        value = Identity.model_validate_json(json.dumps(payload))
        return {"value": value.model_dump(by_alias=True)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The example validates JSON using explicit configuration and then dumps with by_alias=True. A client can submit displayName or the permitted internal spelling and receives the same public name field. An unexpected admin key fails extra="forbid" even though the known display name itself is valid. Current Pydantic 2.14 patch releases tightened handling of alias-related extra input, including security fixes in 2.14.2. The local lab uses 2.14.3 and tests the actual configured behavior. Conflicting submitted aliases should not be treated as a trustworthy way to select an internal value or as a reason to silently widen the writable shape.

Focused decision: use the complete example for context

python

from pydantic import AliasChoices, BaseModel, ConfigDict, Field

class Identity(BaseModel):
    model_config = ConfigDict(extra="forbid", validate_by_name=True)
    display_name: str = Field(
        validation_alias=AliasChoices("displayName", "label"),
        serialization_alias="name",
    )
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

Changing extra="forbid" to ignore can conceal misspelled or unauthorized request keys.

Changing extra="forbid" to ignore can conceal misspelled or unauthorized request keys. The rejected admin field would disappear without alerting the caller, which is the controlled counterexample in this lesson. Allowing extras is a separate contract and should never be introduced simply to make a renamed field work.

Also distinguish by_alias on output from which input names validation accepts. A response name does not automatically become a valid input alias. When a service accepts more than one historical name, define a conflict policy and test duplicate spellings. Aliases are a compatibility mechanism, not an authorization mechanism.

Controlled mistake: observe the actual wrong behavior

python

import json
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError

class Identity(BaseModel):
    model_config = ConfigDict(extra="ignore", validate_by_alias=True,
                              validate_by_name=True)
    display_name: str = Field(validation_alias="displayName",
                              serialization_alias="name")

def inspect_example(payload):
    try:
        value = Identity.model_validate_json(json.dumps(payload))
        return {"value": value.model_dump(by_alias=True)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
Changing extra="forbid" to ignore can conceal misspelled or unauthorized request keys. The rejected admin field would disappear without alerting the caller, which is the controlled counterexample in this lesson. Allowing extras is a separate contract and should never be introduced simply to make a renamed field work. Also distinguish by_alias on output from which input names validation accepts. A response name does not automatically become a valid input alias. When a service accepts more than one historical name, define a conflict policy and test duplicate spellings. Aliases are a compatibility mechanism, not an authorization mechanism.

Independent practice: one bounded change

Accept the additional legacy input name label without accepting unrelated keys.

Accept the additional legacy input name label without accepting unrelated keys. Use AliasChoices for validation and preserve the existing internal and response names. The new label-only input should produce the same public name field as a displayName input.

Test the new name first, then a canonical accepted input and an unknown admin field. The exercise changes only the input-name vocabulary. Do not switch to extra="allow", change the serialization alias, or write a catch-all dictionary parser that discards the model boundary.

Step 1: Accept label as an additional input alias while preserving name output and rejecting unknown keys. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

import json
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError

class Identity(BaseModel):
    model_config = ConfigDict(extra="forbid", validate_by_alias=True,
                              validate_by_name=True)
    display_name: str = Field(validation_alias="displayName",
                              serialization_alias="name")

def inspect_example(payload):
    try:
        value = Identity.model_validate_json(json.dumps(payload))
        return {"value": value.model_dump(by_alias=True)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}

# Independent task: Accept label as an additional input alias while preserving name output and rejecting unknown keys.
Reasoning
Accept label as an additional input alias while preserving name output and rejecting unknown keys.
  • Hint 1: AliasChoices is already imported.
  • Hint 2: Replace the one validation_alias value; keep extra="forbid" and serialization_alias="name".

Solution, tests and trade-offs

The solution uses AliasChoices("displayName", "label") for validation_alias.

The solution uses AliasChoices("displayName", "label") for validation_alias. validate_by_name continues to allow display_name, and by_alias output continues to use name. The practice fixture confirms that the extra-key rejection survives the compatibility change.

A compatibility window also needs an operational migration plan: document supported names, observe safe aggregate usage and eventually remove an obsolete spelling with a versioned change if appropriate. The model alone cannot decide how long legacy clients should be supported. Keep these lifecycle decisions separate from the mechanics of parsing a request.

Practice solution and acceptance checks

python

import json
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError

class Identity(BaseModel):
    model_config = ConfigDict(extra="forbid", validate_by_alias=True,
                              validate_by_name=True)
    display_name: str = Field(validation_alias=AliasChoices("displayName", "label"),
                              serialization_alias="name")

def inspect_example(payload):
    try:
        value = Identity.model_validate_json(json.dumps(payload))
        return {"value": value.model_dump(by_alias=True)}
    except ValidationError as error:
        return {"error": sorted({e["type"] for e in error.errors()})}
Reasoning
The solution uses AliasChoices("displayName", "label") for validation_alias. validate_by_name continues to allow display_name, and by_alias output continues to use name. The practice fixture confirms that the extra-key rejection survives the compatibility change. A compatibility window also needs an operational migration plan: document supported names, observe safe aggregate usage and eventually remove an obsolete spelling with a versioned change if appropriate. The model alone cannot decide how long legacy clients should be supported. Keep these lifecycle decisions separate from the mechanics of parsing a request.

Interview reasoning and next step

Explain the three names with one concrete example in a review: submitted key, Python attribute, emitted key.

Explain the three names with one concrete example in a review: submitted key, Python attribute, emitted key. Then identify a case that would expose accidental acceptance of an unknown writable field. That is more useful than treating aliases as cosmetic renaming.

The next lesson chooses among different model shapes using a discriminator. Both topics make external contracts explicit, but a discriminator selects the kind of object while an alias selects a name for a field. Keep those decisions separate so a migration does not accidentally alter which event or command variants are accepted.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track pydantic, then edit the working exercise baseline and run --practice pyd-aliases. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Separate validation names, internal names and response names, and reject unexpected siblings.