github/spec-kit · error · BundlerError

Each installed-bundle record must be a mapping.

Error message

Each installed-bundle record must be a mapping.

What it means

Raised by InstalledBundleRecord.from_dict when an entry in the installed-bundles records file (.specify/… installed-bundles JSON) is not a JSON object. Each record must be a mapping with bundle_id, version, installed_at, and contributed_components; list or scalar entries indicate corruption.

Source

Thrown at src/specify_cli/bundler/models/records.py:57

            version=version,
            contributed_components=tuple(components),
            installed_at=installed_at or _utc_now(),
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "bundle_id": self.bundle_id,
            "version": self.version,
            "installed_at": self.installed_at,
            "contributed_components": [
                _component_to_dict(c) for c in self.contributed_components
            ],
        }

    @classmethod
    def from_dict(cls, data: Any) -> "InstalledBundleRecord":
        if not isinstance(data, dict):
            raise BundlerError("Each installed-bundle record must be a mapping.")
        components_raw = data.get("contributed_components")
        if components_raw is None:
            components_raw = []
        elif not isinstance(components_raw, list):
            # `or []` would coerce a FALSY non-list (0, '', False, {}) to []
            # before this guard, silently accepting a corrupt record; only an
            # absent/None value means "no components".
            raise BundlerError(
                "Corrupt record: 'contributed_components' must be a list."
            )
        bundle_id = str(data.get("bundle_id", "")).strip()
        version = str(data.get("version", "")).strip()
        if not bundle_id:
            raise BundlerError(
                "Corrupt records file: an installed-bundle record is missing "
                "its 'bundle_id'."
            )
        if not version:

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Restore the entry to an object with bundle_id, version, installed_at, contributed_components.
  2. If the record's bundle is gone, remove the whole entry from 'bundles' rather than deforming it.
  3. Regenerate state by reinstalling the bundle after deleting the corrupt record.

Example fix

// before (installed-bundles.json)
"bundles": ["my-bundle"]

// after
"bundles": [
  {
    "bundle_id": "my-bundle",
    "version": "1.0.0",
    "installed_at": "2026-01-01T00:00:00Z",
    "contributed_components": []
  }
]
Defensive patterns

Strategy: validation

Validate before calling

import json
from pathlib import Path

def records_entries_are_objects(path: Path) -> bool:
    data = json.loads(path.read_text())
    return all(isinstance(b, dict) for b in data.get("bundles", []))

Type guard

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

Try / catch

from specify_cli.bundler.models.records import load_records, BundlerError
try:
    records = load_records(project_root)
except BundlerError as e:
    if "must be a mapping" in str(e):
        # quarantine the file, reinstall bundles to rebuild state
        ...

Prevention

When it happens

Trigger: load_records reads the records file, iterates its 'bundles' list, and calls InstalledBundleRecord.from_dict(item) — an item that is a string, number, or array trips the isinstance(data, dict) guard.

Common situations: Hand-editing the records JSON and turning a record into a bare bundle-id string; a partially written file after a crash; a merge conflict resolved incorrectly.

Related errors


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