github/spec-kit · error · ValueError
Cannot restore '{pack_id}': metadata must be a dict
Error message
Cannot restore '{pack_id}': metadata must be a dict What it means
PresetRegistry.restore_backup rejects a metadata argument that is None or not a dict. restore_backup is used for rollback scenarios and expects a complete registry entry (including installed_at) captured earlier via get(); anything else cannot be restored verbatim.
Source
Thrown at src/specify_cli/presets/__init__.py:661
packs[pack_id] = merged
self._save()
def restore(self, pack_id: str, metadata: dict):
"""Restore preset 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:
pack_id: Preset ID
metadata: Complete preset 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 '{pack_id}': metadata must be a dict")
# Ensure presets dict exists (handle corrupted registry)
if not isinstance(self.data.get("presets"), dict):
self.data["presets"] = {}
self.data["presets"][pack_id] = copy.deepcopy(metadata)
self._save()
def get(self, pack_id: str) -> Optional[dict]:
"""Get preset metadata from registry.
Returns a deep copy to prevent callers from accidentally mutating
nested internal registry state without going through the write path.
Args:
pack_id: Preset ID
Returns:
Deep copy of preset metadata, or None if not found or corrupted
"""View on GitHub (pinned to bf88c9f9a8)
Solutions
- Pass the exact dict previously returned by registry.get(pack_id) (or a deep copy of it).
- Guard before calling: only restore when the backup value is a non-None dict.
- If restoring from a JSON backup file, index into the `presets[pack_id]` entry, not the file root.
Example fix
# before
backup = registry.get(pack_id) # may be None
registry.restore_backup(pack_id, backup)
# after
backup = registry.get(pack_id)
if isinstance(backup, dict):
registry.restore_backup(pack_id, backup) Defensive patterns
Strategy: type-guard
Validate before calling
backup = registry.get(pack_id)
if not isinstance(backup, dict):
raise RuntimeError(f"no valid backup entry for {pack_id}")
registry.restore_backup(pack_id, backup) Type guard
def is_restorable(metadata) -> bool:
return isinstance(metadata, dict) Try / catch
try:
registry.restore_backup(pack_id, metadata)
except ValueError as e:
# metadata was None/malformed: re-capture via registry.get() before rollback
... Prevention
- Capture backups with registry.get() and validate they are dicts before storing.
- When persisting backups as JSON, index the specific presets[pack_id] entry.
- Never pass a get() result through unchecked when the preset may be absent.
When it happens
Trigger: Calling registry.restore_backup(pack_id, metadata) with metadata=None, a JSON-decoded list/string, or passing the whole registry dict instead of a single preset entry.
Common situations: Rollback code that captured `registry.get(pack_id)` returning None for an uninstalled preset and later feeds that None back to restore; deserializing a backup with json.load of the wrong node.
Related errors
- Cannot restore '{extension_id}': metadata must be a dict
- Preset '{pack_id}' not found in registry
- Invalid version specifier: expected a string, got {type(requ
- Failed to parse preset manifest {manifest_path}: {exc}
- Wrap layer {path} is missing {placeholder}
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/15adc9e955a216d6.
Report an issue: GitHub.