JuliusBrussee/caveman · error · CatalogError
: every catalog row must be an object
Error message
{path}: every catalog row must be an object What it means
The catalog YAML file parsed successfully, but at least one element of the top-level list is not a mapping (dict). The validator requires the document to be a list of row objects so each row can be key-checked; a scalar, string, or nested list element cannot represent a catalog row. Thrown by load_yaml before any per-row validation runs.
Solutions
- Open the named file and find the list item that is not a mapping — check indentation and that every row starts with `- key:` at a consistent indent level
- Fix the broken row's indentation (a catalog row must be `- provider: ...` followed by sibling keys at the same deeper indent)
- Remove leftover merge-conflict markers or stray scalar lines from the top-level list
- Re-run the validator (or `python shared/provider-catalog/validate_catalog.py`) to confirm
Example fix
# before - provider: openai model: gpt-4o string-row-not-a-mapping # after - provider: openai model: gpt-4o - provider: anthropic model: claude-sonnet-4
Defensive patterns
Strategy: validation
Validate before calling
import yaml
from pathlib import Path
def rows_are_objects(path: str) -> bool:
doc = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
return isinstance(doc, list) and bool(doc) and all(isinstance(r, dict) for r in doc) Type guard
def is_row_list(doc: object) -> bool:
return isinstance(doc, list) and all(isinstance(r, dict) for r in doc) Try / catch
from shared.provider_catalog.validate_catalog import CatalogError
try:
validate_catalog()
except CatalogError as e:
print(f"catalog invalid: {e}", file=__import__('sys').stderr) Prevention
- Validate current.yaml with a YAML linter/schema check before committing
- Keep every row's indentation identical (consistent two-space steps)
- Never hand-edit the top-level list structure; copy an existing row and modify values
When it happens
Trigger: yaml.safe_load on the catalog file returns a list containing a non-dict item, e.g. a stray `- some string` line between rows, a row indented so it merges into a scalar, or a duplicated top-level key collapsing entries. Any call path through validate_catalog -> load_yaml hits this.
Common situations: Hand-editing current.yaml and accidentally un-indenting a row so it becomes a bare scalar; a merge conflict resolved leaving a leftover `======="` fragment that parses as a list item; a script appending rows with wrong indentation; copy-pasting a row without the leading `- `.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/e5be519ad1b515a9.
Report an issue: GitHub.
Appendix: source
Thrown at shared/provider-catalog/validate_catalog.py:97
# own pricing page; until then, the catalog is invalid.
# MUST stay a prefix of REVIEW_MARKER_PREFIX in that script (the test suite
# asserts it), or a renamed marker would sail past this rule.
REVIEW_MARKER = "# proposed-by:"
class CatalogError(ValueError):
pass
def load_yaml(path: Path) -> list[dict[str, Any]]:
try:
document = yaml.safe_load(path.read_text(encoding="utf-8"))
except (OSError, yaml.YAMLError) as error:
raise CatalogError(f"{path}: invalid YAML: {error}") from error
if not isinstance(document, list) or not document:
raise CatalogError(f"{path}: catalog must be a non-empty list")
if not all(isinstance(row, dict) for row in document):
raise CatalogError(f"{path}: every catalog row must be an object")
return document
def parsed_time(value: Any, label: str) -> datetime:
if isinstance(value, datetime):
parsed = value
elif isinstance(value, str):
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as error:
raise CatalogError(f"{label}: verified_at must be RFC3339") from error
else:
raise CatalogError(f"{label}: verified_at must be an RFC3339 timestamp")
if parsed.tzinfo is None:
raise CatalogError(f"{label}: verified_at must include a timezone")
return parsed.astimezone(timezone.utc)
View on GitHub (pinned to 3ee70a1026)