{"record":{"id":"d67980c1a8ac901f","repo":"github/spec-kit","slug":"cannot-restore-extension-id-metadata-must-be","errorCode":null,"errorMessage":"Cannot restore '{extension_id}': metadata must be a dict","messagePattern":"Cannot restore '(.+?)': metadata must be a dict","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":888,"sourceCode":"        extensions[extension_id] = merged\n        self._save()\n\n    def restore(self, extension_id: str, metadata: dict):\n        \"\"\"Restore extension metadata to registry without modifying timestamps.\n\n        Use this method for rollback scenarios where you have a complete backup\n        of the registry entry (including installed_at) and want to restore it\n        exactly as it was.\n\n        Args:\n            extension_id: Extension ID\n            metadata: Complete extension metadata including installed_at\n\n        Raises:\n            ValueError: If metadata is None or not a dict\n        \"\"\"\n        if metadata is None or not isinstance(metadata, dict):\n            raise ValueError(\n                f\"Cannot restore '{extension_id}': metadata must be a dict\"\n            )\n        # Ensure extensions dict exists (handle corrupted registry)\n        if not isinstance(self.data.get(\"extensions\"), dict):\n            self.data[\"extensions\"] = {}\n        self.data[\"extensions\"][extension_id] = copy.deepcopy(metadata)\n        self._save()\n\n    def remove(self, extension_id: str):\n        \"\"\"Remove extension from registry.\n\n        Args:\n            extension_id: Extension ID\n        \"\"\"\n        extensions = self.data.get(\"extensions\")\n        if not isinstance(extensions, dict):\n            return\n        if extension_id in extensions:","sourceCodeStart":870,"sourceCodeEnd":906,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L870-L906","documentation":"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.","triggerScenarios":"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.","commonSituations":"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().","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()."],"exampleFix":"// before\nregistry.restore(\"myext\", backup_json_string)  # ValueError\n// after\nimport json\nregistry.restore(\"myext\", json.loads(backup_json_string))","handlingStrategy":"type-guard","validationCode":"if not isinstance(backup_metadata, dict):\n    raise SystemExit(\"restore() requires the full metadata dict captured before the change\")\nregistry.restore(extension_id, backup_metadata)","typeGuard":"def is_metadata_dict(metadata: object) -> bool:\n    return isinstance(metadata, dict)","tryCatchPattern":"try:\n    registry.restore(extension_id, metadata)\nexcept ValueError as e:\n    if \"metadata must be a dict\" in str(e):\n        # parse the serialized backup (json.loads) or capture the entry pre-change\n        ...","preventionTips":["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."],"tags":["extensions","registry","rollback","value-error"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}