github/spec-kit · error · BundlerError

Each catalog source must be a mapping.

Error message

Each catalog source must be a mapping.

What it means

Raised by CatalogSource.from_dict() when a single catalog source entry in configuration is not a JSON/YAML object (dict). Sources are loaded from project and user config files whose top-level shape must be a list of mappings; any scalar, list, or null element fails this guard before field validation begins.

Solutions

  1. Make each element of the sources list an object with at least id, url, priority, and install_policy keys.
  2. Validate the config with a JSON linter or json.load plus an isinstance(item, dict) check before relying on it.
  3. Regenerate the config through add_source() instead of editing the file by hand.

Example fix

# before (config file)
[{"id": "a", "url": "https://x/c.json", "priority": 5, "install_policy": "install-allowed"}, "https://y/c.json"]

# after
[{"id": "a", "url": "https://x/c.json", "priority": 5, "install_policy": "install-allowed"},
 {"id": "b", "url": "https://y/c.json", "priority": 5, "install_policy": "install-allowed"}]
Defensive patterns

Strategy: type-guard

Validate before calling

import json

data = json.loads(config_text)
assert isinstance(data, list), "catalog config must be a list"
for item in data:
    if not isinstance(item, dict):
        raise ValueError(f"catalog source entry is not an object: {item!r}")

Type guard

def is_source_mapping(item: object) -> bool:
    return isinstance(item, dict)

Try / catch

try:
    CatalogSource.from_dict(item, Scope.PROJECT)
except BundlerError as e:
    log_config_error(config_path, e)  # point the user at the offending entry

Prevention

When it happens

Trigger: A catalog config file whose 'sources'-style list contains a bare string (e.g. a url) instead of an object; a YAML dash-item that parsed as a scalar; a null entry from a trailing comma or malformed JSON merge.

Common situations: Hand-editing the catalog config and writing just a url per line; JSON produced by string concatenation instead of a serializer; config templating that renders an empty placeholder value.

Related errors


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

Appendix: source

Thrown at src/specify_cli/bundler/models/catalog.py:71

)


@dataclass(frozen=True)
class CatalogSource:
    id: str
    url: str
    priority: int
    install_policy: InstallPolicy
    scope: Scope = Scope.PROJECT

    @property
    def install_allowed(self) -> bool:
        return self.install_policy is InstallPolicy.INSTALL_ALLOWED

    @classmethod
    def from_dict(cls, data: Any, scope: Scope) -> "CatalogSource":
        if not isinstance(data, dict):
            raise BundlerError("Each catalog source must be a mapping.")
        source_id = str(data.get("id", "")).strip()
        url = str(data.get("url", "")).strip()
        if not source_id:
            raise BundlerError("A catalog source is missing its 'id'.")
        if not url:
            raise BundlerError(f"Catalog source '{source_id}' is missing its 'url'.")
        priority = data.get("priority")
        if priority is None:
            raise BundlerError(f"Catalog source '{source_id}' is missing its 'priority'.")
        if isinstance(priority, bool) or not isinstance(priority, (int, str)):
            raise BundlerError(
                f"Catalog source '{source_id}' has a non-integer priority: {priority!r}."
            )
        try:
            priority_int = int(priority)
        except (TypeError, ValueError):
            raise BundlerError(
                f"Catalog source '{source_id}' has a non-integer priority: {priority!r}."

View on GitHub (pinned to bf88c9f9a8)