github/spec-kit · error · ValidationError
Invalid extension: expected a mapping, got {type(ext).__name
Error message
Invalid extension: expected a mapping, got {type(ext).__name__} What it means
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.
Source
Thrown at src/specify_cli/extensions/__init__.py:292
)
# The REQUIRED_FIELDS loop above only checks key PRESENCE, so a section
# that is written but left empty (``provides:`` -> None) or given the
# wrong shape (``provides: []``) passes it and then fails on first use:
# ``field not in None`` raises TypeError and ``None.get(...)`` raises
# AttributeError. Neither is a ValidationError, so both escape the
# callers that already handle malformed manifests -- list_installed()'s
# "Corrupted extension" fallback catches ValidationError only, so one bad
# extension made ``specify extension list`` exit 1 with a raw
# AttributeError instead of listing the rest. Guard each required
# section's shape, mirroring the nested guards below ("Invalid
# provides.commands: expected a list", "Invalid hooks: expected a
# mapping") and _load_yaml's document-root check.
# Validate extension metadata
ext = self.data["extension"]
if not isinstance(ext, dict):
raise ValidationError(
f"Invalid extension: expected a mapping, got {type(ext).__name__}"
)
# Check presence AND type: the format/version checks below feed these
# values straight to ``re.match`` and ``packaging.Version``, both of
# which raise a bare TypeError on a non-string. YAML makes that an easy
# authoring slip -- unquoted ``version: 1.0`` parses as a float and
# ``id: 2`` as an int -- and TypeError is not a ValidationError, so it
# escapes every caller that already handles a malformed manifest (see
# list_installed()'s "Corrupted extension" fallback, which catches
# ValidationError only, making one bad extension exit ``specify
# extension list`` with a raw traceback and hide the healthy ones).
# Mirrors the sibling IntegrationDescriptor, which already type-checks
# the same four fields.
for field in ["id", "name", "version", "description"]:
if field not in ext:
raise ValidationError(f"Missing extension.{field}")
if not isinstance(ext[field], str):
raise ValidationError(View on GitHub (pinned to bf88c9f9a8)
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.
Example fix
# before extension: my-ext # after extension: id: my-ext name: My Extension version: 1.0.0 description: Does things
Defensive patterns
Strategy: type-guard
Validate before calling
ext = data.get("extension")
if not isinstance(ext, dict):
raise SystemExit("extension section must be a mapping") Type guard
def extension_section_is_mapping(data: dict) -> bool:
return isinstance(data.get("extension"), dict) Try / catch
except ValidationError as e:
if str(e).startswith("Invalid extension: expected a mapping"):
rewrite_extension_as_mapping(path) Prevention
- Always fill the extension: block with its four fields before installing.
- Validate section shapes (not just key presence) in manifest CI checks.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Manifest must be a YAML mapping, got {type(data).__name__}:
- Invalid extension.{field}: expected a string, got {type(ext[
- Manifest must be a YAML mapping at the top level.
- Manifest is missing the required 'bundle' mapping.
- 'requires' must be a mapping when present.
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/d7cec180e6cadb69.
Report an issue: GitHub.