github/spec-kit · error · BundlerError
Malformed catalog config at {config_path}: expected a mappin
Error message
Malformed catalog config at {config_path}: expected a mapping at the top level, got {type(data).__name__}. What it means
Raised by the bundle-catalog config reader when a bundle-catalogs.yml file parses to something other than a YAML mapping at the top level (e.g. the file contains a list, scalar, or is empty-but-typed like '[]' or 'false'). The bundler merges catalog sources per scope (user/project), and a non-mapping document cannot be merged, so it fails fast with the offending path and detected type name.
Source
Thrown at src/specify_cli/bundler/models/catalog.py:278
project_config = Path(project_root) / ".specify" / CONFIG_FILENAME
if project_config.exists():
ensure_within(project_root, project_config)
_merge_config(by_id, project_config, Scope.PROJECT)
return sorted(by_id.values(), key=lambda s: (s.priority, s.id))
def _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Scope) -> None:
if not config_path.exists():
return
# ``load_yaml`` returns ``{}`` only for an empty document and the raw parse
# otherwise, so a non-mapping top level (a YAML list or scalar, including
# the falsy ``[]``/``false``/``0``/``''``) is caught here and raised —
# matching the sibling reader commands_impl/catalog_config._read. #3623
# aligned the inner non-list ``catalogs`` value between the two readers.
data = load_yaml(config_path)
if not isinstance(data, dict):
raise BundlerError(
f"Malformed catalog config at {config_path}: expected a mapping at "
f"the top level, got {type(data).__name__}."
)
# Reject an unsupported major schema version, matching the sibling reader
# commands_impl/catalog_config._read. Without this, a file written by a
# newer/incompatible Spec Kit was silently parsed under v1 assumptions on
# the resolution path (bundle search/install), while the other reader
# rejected it — the two readers disagreed. An absent schema_version stays
# valid (backward compatible with configs that omit it).
schema_version = data.get("schema_version")
if schema_version is not None and (
str(schema_version).strip().split(".")[0]
!= CONFIG_SCHEMA_VERSION.split(".")[0]
):
raise BundlerError(
f"Unsupported catalog config schema version "
f"'{str(schema_version).strip()}' at {config_path}; this Spec Kit "
f"understands version {CONFIG_SCHEMA_VERSION}. The file may have been "View on GitHub (pinned to bf88c9f9a8)
Solutions
- Open the file named in the error and make the top level a YAML mapping, e.g. 'schema_version: 1' / 'catalogs:' keys at column 0.
- Remove any leading '-' sequence entries or stray scalars at the top level.
- Validate the file with a YAML linter or 'python -c "import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(type(d))"' expecting dict.
- If the file is unwanted, delete it — _merge_config returns early when the path does not exist.
Example fix
# before (bundle-catalogs.yml)
- id: my-catalog
url: https://example.com/catalog.yml
# after
schema_version: 1
catalogs:
- id: my-catalog
url: https://example.com/catalog.yml Defensive patterns
Strategy: validation
Validate before calling
import yaml
from pathlib import Path
def catalog_config_is_valid(path: Path) -> bool:
if not path.exists():
return True # absent config is fine
data = yaml.safe_load(path.read_text())
return data is None or isinstance(data, dict) Type guard
def is_catalog_mapping(data: object) -> bool:
return data is None or isinstance(data, dict) Try / catch
from specify_cli.bundler.models.catalog import BundlerError
try:
resolve_catalogs(...)
except BundlerError as e:
if "Malformed catalog config" in str(e):
# show path + type from message, prompt user to fix or delete file
... Prevention
- Lint bundle-catalogs.yml in CI with a YAML schema checker that requires a top-level mapping.
- Never hand-wrap catalog entries in a top-level YAML sequence.
- Generate catalog config via specify tooling instead of manual editing.
When it happens
Trigger: Calling the catalog resolution path (bundle search/install) while a bundle-catalogs.yml exists whose top-level node is a sequence (starts with '- '), a bare scalar, or a falsy value like '[]', 'false', '0', or an empty quoted string. _merge_config(by_id, config_path, scope) hits the isinstance(data, dict) check after load_yaml.
Common situations: Hand-editing bundle-catalogs.yml and accidentally putting the 'catalogs:' key under a list item; pasting a YAML fragment that starts with '- '; truncating the file so only a scalar remains; converting the file from another format and losing the top-level mapping.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Malformed catalog config at {path}: expected a mapping at th
- Malformed catalog config at {config_path}: 'catalogs' must b
- Malformed catalog config at {path}: 'catalogs' must be a lis
- Malformed catalog config at {path}: each catalog entry must
- A catalog url is required.
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/6d3fc22fa4d2df20.
Report an issue: GitHub.