diff --git a/builders/server/core/runtime/validator.py b/builders/server/core/runtime/validator.py index b59ce5d..0386a03 100644 --- a/builders/server/core/runtime/validator.py +++ b/builders/server/core/runtime/validator.py @@ -23,7 +23,15 @@ def validate(data: dict, schema: dict[str, SchemaType]) -> None: ) -def validate_rows(data_list: list[dict], schema: dict[str, SchemaType]) -> None: +def validate_rows(data_list: object, schema: dict[str, SchemaType]) -> None: """Validate each dict in a list against the declared schema.""" - for data in data_list: + if not isinstance(data_list, list): + raise ValidationError("Builder output must be a list of rows") + + for index, data in enumerate(data_list): + if not isinstance(data, dict): + raise ValidationError( + f"Builder output row {index} must be a dict, got " + f"'{type(data).__name__}'" + ) validate(data, schema) diff --git a/builders/server/tests/core/runtime/test_validator.py b/builders/server/tests/core/runtime/test_validator.py index d8ee66c..dfea28c 100644 --- a/builders/server/tests/core/runtime/test_validator.py +++ b/builders/server/tests/core/runtime/test_validator.py @@ -82,6 +82,19 @@ def test_validate_rows_empty_list() -> None: validate_rows([], {"ticker": SchemaType.STR}) +@pytest.mark.parametrize("data_list", [None, {}]) +def test_validate_rows_rejects_non_list_output(data_list: object) -> None: + """Builder outputs must be lists of row dictionaries.""" + with pytest.raises(ValidationError, match="must be a list of rows"): + validate_rows(data_list, {"ticker": SchemaType.STR}) + + +def test_validate_rows_rejects_non_dict_item() -> None: + """Every row in the builder output must be a dictionary.""" + with pytest.raises(ValidationError, match="row 1 must be a dict"): + validate_rows([{"ticker": "AAPL"}, "MSFT"], {"ticker": SchemaType.STR}) + + def test_validate_rows_invalid_item_raises() -> None: """Invalid item in the list raises ValidationError.""" with pytest.raises(ValidationError, match="Missing key 'price'"):