Katkat

Debug Validation Error Aggregation

StripeSoftware Engineer · Onsite

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

  1. 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 name is 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 "".
  2. 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.

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,000 total nodes.
  • Maximum nesting depth is 500 levels.
  • name strings 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?