github/spec-kit · error · BundlerError

Malformed catalog config at {path}: each catalog entry must

Error message

Malformed catalog config at {path}: each catalog entry must be a mapping, got {type(entry).__name__}.

What it means

Raised during the per-entry loop over `catalogs` when an individual entry is not a mapping. Each catalog source must be a mapping with at least `id`, `url`, `priority`, `install_policy`; a bare string or nested list inside `catalogs` is rejected here.

Source

Thrown at src/specify_cli/bundler/commands_impl/catalog_config.py:74

        != CONFIG_SCHEMA_VERSION.split(".")[0]
    ):
        raise BundlerError(
            f"Unsupported catalog config schema version "
            f"'{str(schema_version).strip()}' at {path}; this Spec Kit "
            f"understands version {CONFIG_SCHEMA_VERSION}. The file may have been "
            "written by a newer version or is corrupt."
        )
    catalogs = data.get("catalogs")
    if catalogs is None:
        return []
    if not isinstance(catalogs, list):
        raise BundlerError(
            f"Malformed catalog config at {path}: 'catalogs' must be a list, "
            f"got {type(catalogs).__name__}."
        )
    for entry in catalogs:
        if not isinstance(entry, dict):
            raise BundlerError(
                f"Malformed catalog config at {path}: each catalog entry must be "
                f"a mapping, got {type(entry).__name__}."
            )
    return list(catalogs)


def _write(project_root: Path, catalogs: list[dict]) -> None:
    payload = {"schema_version": CONFIG_SCHEMA_VERSION, "catalogs": catalogs}
    dump_yaml(_config_path(project_root), payload, within=project_root)


def _slug(value: str) -> str:
    # Lowercase so derived ids are deterministic and case-insensitive across
    # platforms (e.g. 'Team-A.json' and 'team-a.json' yield the same id),
    # keeping the case-sensitive duplicate check from admitting logical dupes.
    return "".join(ch if ch.isalnum() else "-" for ch in value.lower()).strip("-")

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Convert each entry to a mapping with `id`, `url`, `priority`, and `install_policy` keys
  2. Remove stray scalar entries
  3. Let `add_source` generate well-formed entries for you

Example fix

# before
catalogs:
  - https://example.com/c.json

# after
catalogs:
  - id: example
    url: https://example.com/c.json
    priority: 50
    install_policy: auto
Defensive patterns

Strategy: validation

Validate before calling

for i, entry in enumerate(data.get("catalogs") or []):
    if not isinstance(entry, dict):
        raise SystemExit(f"catalogs[{i}] must be a mapping with id/url/priority/install_policy")

Type guard

def all_entries_are_mappings(data: dict) -> bool:
    return all(isinstance(e, dict) for e in (data.get("catalogs") or []))

Try / catch

try:
    read_catalog_config(project_root)
except BundlerError as exc:
    if "each catalog entry must be a mapping" in str(exc):
        # replace scalar entries with {id, url, priority, install_policy} objects
        raise
    raise

Prevention

When it happens

Trigger: `catalogs: ["community", "https://..."]` — a list of strings instead of a list of objects; or `- - id: x` producing a nested list.

Common situations: Shorthand attempt to list just URLs; converting from a simple `.gitignore`-style list format; a bad merge that collapsed entries.

Understand the failure class

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/8165e76e7242fbb0. Report an issue: GitHub.