github/spec-kit · error · ValueError

Cannot restore '{extension_id}': metadata must be a dict

Error message

Cannot restore '{extension_id}': metadata must be a dict

What it means

ExtensionRegistry.restore() raised ValueError because the metadata argument is None or not a dict. restore() is the rollback path: it writes a COMPLETE backed-up registry entry (including installed_at) verbatim after a deepcopy, so it cannot accept partial or non-mapping input — unlike update_extension_metadata(), which merges patch-style into existing data.

Source

Thrown at src/specify_cli/extensions/__init__.py:888

        extensions[extension_id] = merged
        self._save()

    def restore(self, extension_id: str, metadata: dict):
        """Restore extension metadata to registry without modifying timestamps.

        Use this method for rollback scenarios where you have a complete backup
        of the registry entry (including installed_at) and want to restore it
        exactly as it was.

        Args:
            extension_id: Extension ID
            metadata: Complete extension metadata including installed_at

        Raises:
            ValueError: If metadata is None or not a dict
        """
        if metadata is None or not isinstance(metadata, dict):
            raise ValueError(
                f"Cannot restore '{extension_id}': metadata must be a dict"
            )
        # Ensure extensions dict exists (handle corrupted registry)
        if not isinstance(self.data.get("extensions"), dict):
            self.data["extensions"] = {}
        self.data["extensions"][extension_id] = copy.deepcopy(metadata)
        self._save()

    def remove(self, extension_id: str):
        """Remove extension from registry.

        Args:
            extension_id: Extension ID
        """
        extensions = self.data.get("extensions")
        if not isinstance(extensions, dict):
            return
        if extension_id in extensions:

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Pass the full metadata dict captured before the change, e.g. backup = registry.data['extensions']['myext'] then later registry.restore('myext', backup).
  2. If the backup arrived serialized, parse it first: json.loads(payload).
  3. For partial updates use update_extension_metadata() instead of restore().

Example fix

// before
registry.restore("myext", backup_json_string)  # ValueError
// after
import json
registry.restore("myext", json.loads(backup_json_string))
Defensive patterns

Strategy: type-guard

Validate before calling

if not isinstance(backup_metadata, dict):
    raise SystemExit("restore() requires the full metadata dict captured before the change")
registry.restore(extension_id, backup_metadata)

Type guard

def is_metadata_dict(metadata: object) -> bool:
    return isinstance(metadata, dict)

Try / catch

try:
    registry.restore(extension_id, metadata)
except ValueError as e:
    if "metadata must be a dict" in str(e):
        # parse the serialized backup (json.loads) or capture the entry pre-change
        ...

Prevention

When it happens

Trigger: Calling restore('myext', None), restore('myext', "enabled=true"), or restore('myext', ["enabled"]) — e.g. a rollback handler passing a serialized/None backup instead of the parsed dict snapshot captured before the operation.

Common situations: Rollback code paths where the backup variable was never assigned (None default); passing a JSON string instead of json.loads(result); reusing update()-style partial patches with restore().

Related errors


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