{"record":{"id":"d7cec180e6cadb69","repo":"github/spec-kit","slug":"invalid-extension-expected-a-mapping-got-type-e","errorCode":null,"errorMessage":"Invalid extension: expected a mapping, got {type(ext).__name__}","messagePattern":"Invalid extension: expected a mapping, got (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":292,"sourceCode":"            )\n\n        # The REQUIRED_FIELDS loop above only checks key PRESENCE, so a section\n        # that is written but left empty (``provides:`` -> None) or given the\n        # wrong shape (``provides: []``) passes it and then fails on first use:\n        # ``field not in None`` raises TypeError and ``None.get(...)`` raises\n        # AttributeError. Neither is a ValidationError, so both escape the\n        # callers that already handle malformed manifests -- list_installed()'s\n        # \"Corrupted extension\" fallback catches ValidationError only, so one bad\n        # extension made ``specify extension list`` exit 1 with a raw\n        # AttributeError instead of listing the rest. Guard each required\n        # section's shape, mirroring the nested guards below (\"Invalid\n        # provides.commands: expected a list\", \"Invalid hooks: expected a\n        # mapping\") and _load_yaml's document-root check.\n\n        # Validate extension metadata\n        ext = self.data[\"extension\"]\n        if not isinstance(ext, dict):\n            raise ValidationError(\n                f\"Invalid extension: expected a mapping, got {type(ext).__name__}\"\n            )\n        # Check presence AND type: the format/version checks below feed these\n        # values straight to ``re.match`` and ``packaging.Version``, both of\n        # which raise a bare TypeError on a non-string. YAML makes that an easy\n        # authoring slip -- unquoted ``version: 1.0`` parses as a float and\n        # ``id: 2`` as an int -- and TypeError is not a ValidationError, so it\n        # escapes every caller that already handles a malformed manifest (see\n        # list_installed()'s \"Corrupted extension\" fallback, which catches\n        # ValidationError only, making one bad extension exit ``specify\n        # extension list`` with a raw traceback and hide the healthy ones).\n        # Mirrors the sibling IntegrationDescriptor, which already type-checks\n        # the same four fields.\n        for field in [\"id\", \"name\", \"version\", \"description\"]:\n            if field not in ext:\n                raise ValidationError(f\"Missing extension.{field}\")\n            if not isinstance(ext[field], str):\n                raise ValidationError(","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L274-L310","documentation":"Raised when the manifest's `extension` section is present but not a mapping (string, list, or null from an empty `extension:`). This guard exists because the old presence-only check let empty/wrong-shape sections through, which then blew up as raw TypeError/AttributeError (`field not in None`) that escaped the ValidationError-only 'Corrupted extension' fallback in list_installed() and made one bad extension crash `specify extension list` for all others.","triggerScenarios":"Manifest contains `extension:` with nothing under it (null), `extension: my-ext` (string), or a list; _validate() reaches the isinstance(ext, dict) check and raises with the actual type name.","commonSituations":"Writing `extension:` and intending to fill it later; inlining the extension id as a string shorthand; indentation errors demoting the extension fields so the section parses as empty.","solutions":["Make `extension` a mapping containing id, name, version, description.","Check indentation — the four fields must be indented under `extension:`.","Re-run specify extension list/install to confirm it now parses."],"exampleFix":"# before\nextension: my-ext\n\n# after\nextension:\n  id: my-ext\n  name: My Extension\n  version: 1.0.0\n  description: Does things","handlingStrategy":"type-guard","validationCode":"ext = data.get(\"extension\")\nif not isinstance(ext, dict):\n    raise SystemExit(\"extension section must be a mapping\")","typeGuard":"def extension_section_is_mapping(data: dict) -> bool:\n    return isinstance(data.get(\"extension\"), dict)","tryCatchPattern":"except ValidationError as e:\n    if str(e).startswith(\"Invalid extension: expected a mapping\"):\n        rewrite_extension_as_mapping(path)","preventionTips":["Always fill the extension: block with its four fields before installing.","Validate section shapes (not just key presence) in manifest CI checks."],"tags":["manifest","validation","extensions","yaml"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}