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

  1. Make `extension` a mapping containing id, name, version, description.
  2. Check indentation — the four fields must be indented under `extension:`.
  3. 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

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


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/d7cec180e6cadb69. Report an issue: GitHub.