Typorithm · medium

Nested errors need a path, not a copied request

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

Step 1 of 4 · intro card

Nested errors need a path, not a copied request

0%

Concept and contract

Keep collection indexes and parent fields when explaining a validation failure.

A structured request can contain a list of nested objects. The information needed to repair it is not just that a country is invalid, but which object and which field failed. Pydantic error locations describe that path using field names and collection indexes.

The Role model has a title and a list of Place objects. Each Place permits one of four country codes in this deliberately small teaching contract and requires a nonempty city. Those choices are fixture policies, not a list of every valid country or a statement about the ApplyDjinn vacancy catalogue.

Mechanism and worked example

When validation succeeds, nested models are dumped into the expected ordinary JSON-compatible structure.

When validation succeeds, nested models are dumped into the expected ordinary JSON-compatible structure. When it fails, inspect_example selects only loc and type from each error. The location is converted into a list so a client can navigate from locations to index zero and then to country.

A malformed country at the second location must retain index one. A blank city is a different failure at its own field. Observe both examples before building a form message. Mapping every error to the final field name alone would make multiple countries or cities indistinguishable in a larger request.

Complete worked example

python

from typing import Literal
from pydantic import BaseModel, Field, ValidationError

class Place(BaseModel):
    country: Literal["US", "IL", "UA", "CA"]
    city: str = Field(min_length=1)

class Role(BaseModel):
    title: str
    locations: list[Place]

def inspect_example(payload):
    try:
        return {"value": Role.model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"errors": [{"path": list(e["loc"]), "type": e["type"]}
                           for e in error.errors(include_input=False)]}
Reasoning
When validation succeeds, nested models are dumped into the expected ordinary JSON-compatible structure. When it fails, inspect_example selects only loc and type from each error. The location is converted into a list so a client can navigate from locations to index zero and then to country. A malformed country at the second location must retain index one. A blank city is a different failure at its own field. Observe both examples before building a form message. Mapping every error to the final field name alone would make multiple countries or cities indistinguishable in a larger request.

Focused decision: use the complete example for context

python

def safe_errors(error):
    return [{"path": list(item["loc"]), "type": item["type"]}
            for item in error.errors(include_input=False)]
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

Validation errors can include submitted input and context.

Validation errors can include submitted input and context. Dumping the entire error structure into a public response or an ordinary log can reveal values that a client did not expect to be retained. The example deliberately exposes only a minimal path and stable type for its synthetic inputs.

Do not claim that every error needs the same handling. A user-facing form may need a safe field-specific explanation, while operational logs need an event identifier and restricted access. The controlled counterexample keeps only the last element of loc, losing the collection and parent path. Its output looks reasonable until a nested acceptance case is examined.

Controlled mistake: observe the actual wrong behavior

python

from typing import Literal
from pydantic import BaseModel, Field, ValidationError

class Place(BaseModel):
    country: Literal["US", "IL", "UA", "CA"]
    city: str = Field(min_length=1)

class Role(BaseModel):
    title: str
    locations: list[Place]

def inspect_example(payload):
    try:
        return {"value": Role.model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"errors": [{"path": [e["loc"][-1]], "type": e["type"]}
                           for e in error.errors(include_input=False)]}
Reasoning
Validation errors can include submitted input and context. Dumping the entire error structure into a public response or an ordinary log can reveal values that a client did not expect to be retained. The example deliberately exposes only a minimal path and stable type for its synthetic inputs. Do not claim that every error needs the same handling. A user-facing form may need a safe field-specific explanation, while operational logs need an event identifier and restricted access. The controlled counterexample keeps only the last element of loc, losing the collection and parent path. Its output looks reasonable until a nested acceptance case is examined.

Independent practice: one bounded change

Extend the teaching contract to permit DE alongside the four existing country codes.

Extend the teaching contract to permit DE alongside the four existing country codes. Preserve the city bound and preserve full error paths for every still-invalid object. This is a change to the Literal alternatives rather than a generic accept-any-string escape.

Test an accepted DE place, an XX rejection and a blank city. Then inspect the location of the failed city in the returned error. The task should not change all error types into a single message or reorder the locations list. A small vocabulary change must leave the useful repair context intact.

Step 1: Permit DE while retaining the nested error path and the nonempty city requirement. 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

from typing import Literal
from pydantic import BaseModel, Field, ValidationError

class Place(BaseModel):
    country: Literal["US", "IL", "UA", "CA"]
    city: str = Field(min_length=1)

class Role(BaseModel):
    title: str
    locations: list[Place]

def inspect_example(payload):
    try:
        return {"value": Role.model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"errors": [{"path": list(e["loc"]), "type": e["type"]}
                           for e in error.errors(include_input=False)]}

# Independent task: Permit DE while retaining the nested error path and the nonempty city requirement.
Reasoning
Permit DE while retaining the nested error path and the nonempty city requirement.
  • Hint 1: Extend only the country Literal in Place.
  • Hint 2: Keep list(e["loc"]) and the Field(min_length=1) city constraint.

Solution, tests and trade-offs

The solution adds one Literal alternative.

The solution adds one Literal alternative. The practice checks verify the new accepted case, keep an unsupported code rejected, and retain the nested city path. The tests compare paths and types rather than copying a full framework error message that can change between versions.

A path is a navigation aid, not a permission check. An attacker still must not learn whether another account owns a particular object. Separate request-shape feedback from authorization and lookup policy in the service. Prefer an explicit output schema for user-visible failures instead of serializing arbitrary exception dictionaries.

Practice solution and acceptance checks

python

from typing import Literal
from pydantic import BaseModel, Field, ValidationError

class Place(BaseModel):
    country: Literal["US", "IL", "UA", "CA", "DE"]
    city: str = Field(min_length=1)

class Role(BaseModel):
    title: str
    locations: list[Place]

def inspect_example(payload):
    try:
        return {"value": Role.model_validate(payload).model_dump()}
    except ValidationError as error:
        return {"errors": [{"path": list(e["loc"]), "type": e["type"]}
                           for e in error.errors(include_input=False)]}
Reasoning
The solution adds one Literal alternative. The practice checks verify the new accepted case, keep an unsupported code rejected, and retain the nested city path. The tests compare paths and types rather than copying a full framework error message that can change between versions. A path is a navigation aid, not a permission check. An attacker still must not learn whether another account owns a particular object. Separate request-shape feedback from authorization and lookup policy in the service. Prefer an explicit output schema for user-visible failures instead of serializing arbitrary exception dictionaries.

Interview reasoning and next step

In an interview, draw a nested location such as locations, 1, country and explain how a client would use it.

In an interview, draw a nested location such as locations, 1, country and explain how a client would use it. State which diagnostic fields are safe for the intended audience, then identify information that must remain restricted. This makes validation feedback a deliberate API contract.

The next topic accepts alternate input names. Nested paths can refer to names chosen by aliases, so make that mapping part of a client migration plan. Never flatten errors merely to avoid thinking about the relationship between external names, internal field names and a stable public response.

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-nested. 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.

Keep collection indexes and parent fields when explaining a validation failure.