Debug Validation Error Aggregation
StripeSoftware Engineer · OnsiteLast updated
Tests the ability to traverse nested validation error trees and normalize them into a flattened dictionary mapping dotted-path keys to aggregated lists of error messages.
Debug Validation Error Aggregation
In complex API platforms, input payloads are validated against hierarchical schemas. When validation fails across deeply nested structures (including nested objects and arrays), libraries raise hierarchical error trees (similar to Colander or Pydantic validation errors). Your task is to aggregate these nested errors into a single, flattened dictionary where keys represent dotted paths and values are lists of error message strings.
Requirements
You are given a tree of ValidationError nodes. Implement the function aggregate_errors(root: ValidationError) -> dict[str, list[str]] that traverses the tree and collects all error messages.
-
Path Construction:
- Paths are constructed by joining ancestor node
names with a dot (.). - If the root node has
name=None, it does not contribute to the path name (its children's names form the base of their paths). - If an intermediate node has
name=None, it should not add a path segment (children attach directly to the current prefix). - For list elements, the
nameis typically a string representing the index (e.g.,'0','1'), resulting in paths like'items.0.price'. - If the root itself has messages and
name=None, its key in the returned dictionary should be the empty string"".
- Paths are constructed by joining ancestor node
-
Message Aggregation:
- A node can contain zero, one, or multiple error messages in
node.messages. - If multiple errors are recorded for the exact same path across the tree, combine their messages into a single list preserving the traversal order (pre-order: node's own messages first, followed by child messages in child list order).
- Do not include paths that have zero total messages.
- A node can contain zero, one, or multiple error messages in
Interface
class ValidationError:
def __init__(self, name: str | None = None, messages: list[str] | None = None, children: list["ValidationError"] | None = None):
self.name = name
self.messages = messages or []
self.children = children or []
def aggregate_errors(root: ValidationError) -> dict[str, list[str]]:
"""
Aggregates a nested ValidationError tree into a dictionary mapping
dotted field paths to lists of validation messages.
"""
pass
Examples
Example 1:
# Root error containing nested object failures
root = ValidationError(
name=None,
children=[
ValidationError(
name="user",
children=[
ValidationError(name="email", messages=["Invalid email format"]),
ValidationError(
name="address",
children=[
ValidationError(name="zip", messages=["Zip code is required", "Must be 5 digits"])
]
)
]
)
]
)
aggregate_errors(root)
# Output:
# {
# "user.email": ["Invalid email format"],
# "user.address.zip": ["Zip code is required", "Must be 5 digits"]
# }
Example 2:
# Root has global messages and child with list indexing
root = ValidationError(
name=None,
messages=["Request payload too large"],
children=[
ValidationError(
name="items",
children=[
ValidationError(name="0", messages=["Item out of stock"]),
ValidationError(name="1", children=[
ValidationError(name="quantity", messages=["Must be positive"])
])
]
)
]
)
aggregate_errors(root)
# Output:
# {
# "": ["Request payload too large"],
# "items.0": ["Item out of stock"],
# "items.1.quantity": ["Must be positive"]
# }
Constraints
- The tree contains at most
10,000total nodes. - Maximum nesting depth is
500levels. namestrings consist of alphanumeric characters and underscores ([a-zA-Z0-9_]), or represent an integer index (0,1, etc.).
Hints
Hint 1
Use a recursive or stack-based depth-first search that tracks the accumulated path segments as a list rather than concatenating strings on every step.
Hint 2
Be careful with how empty strings and `None` names are handled: path segments should only be appended if `name` is not `None` and not empty, except when defining the root key fallback.
Hint 3
Use a `collections.defaultdict(list)` to append error messages in pre-order traversal so that multiple errors for the same path combine cleanly.
Follow-up questions
- How would you handle cyclic references in the error tree if nodes were constructed improperly?
- If validation paths supported wildcard patterns or custom delimiter formatting (e.g., JSON Pointer `"/items/0/price"`), how would you refactor your path resolution logic?
- How would you ensure error aggregation remains performant when dealing with deeply nested trees that might cause a stack overflow in Python recursion?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.