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
- Pass the full metadata dict captured before the change, e.g. backup = registry.data['extensions']['myext'] then later registry.restore('myext', backup).
- If the backup arrived serialized, parse it first: json.loads(payload).
- 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
- Capture the snapshot with backup = registry.data['extensions'][ext_id] before mutating.
- Use update_extension_metadata() for partial patches; restore() is for full verbatim rollback.
- json.loads serialized backups before passing them in.
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
- Extension '{extension_id}' is not installed
- Cannot restore '{pack_id}': metadata must be a dict
- Invalid extension registry {registry.registry_path}: refusin
- Unsupported agent: {agent_name}
- providers[{i}]: unknown provider {provider!r}; registered: {
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/d67980c1a8ac901f.
Report an issue: GitHub.