github/spec-kit · error · ValidationError

Invalid requires: expected a mapping, got {type(requires).__

Error message

Invalid requires: expected a mapping, got {type(requires).__name__}

What it means

The top-level requires section of extension.yml must be a mapping (YAML dict). The validator indexes self.data["requires"] and checks isinstance(requires, dict); a list, string, or scalar fails immediately.

Source

Thrown at src/specify_cli/extensions/__init__.py:346

        # Validate optional category field (free-form string)
        if "category" in ext:
            if not isinstance(ext["category"], str) or not ext["category"].strip():
                raise ValidationError(
                    "Invalid extension.category: must be a non-empty string"
                )

        # Validate optional effect field
        if "effect" in ext:
            if not isinstance(ext["effect"], str) or ext["effect"] not in VALID_EFFECTS:
                raise ValidationError(
                    f"Invalid extension.effect '{ext.get('effect')}': "
                    f"must be one of {sorted(VALID_EFFECTS)}"
                )

        # Validate requires section
        requires = self.data["requires"]
        if not isinstance(requires, dict):
            raise ValidationError(
                f"Invalid requires: expected a mapping, got {type(requires).__name__}"
            )
        if "speckit_version" not in requires:
            raise ValidationError("Missing requires.speckit_version")
        # Presence alone is not enough: check_compatibility() feeds this value to
        # ``SpecifierSet(required)``, guarded only by ``except InvalidSpecifier``,
        # which a non-string escapes two different ways. A float/int/bool/None
        # raises TypeError from the constructor, while a list or dict is an
        # *iterable*, so SpecifierSet accepts it and the failure surfaces much
        # later as ``AttributeError: 'str' object has no attribute 'filter'`` from
        # inside .contains(). Neither is a CompatibilityError, so both bypass the
        # CLI's "Compatibility Error" handler and exit 1 with a raw traceback
        # naming no field. An unquoted ``speckit_version: 1.0`` is an easy YAML
        # slip. Mirrors the sibling IntegrationDescriptor, which already requires
        # a non-empty string here.
        if (
            not isinstance(requires["speckit_version"], str)
            or not requires["speckit_version"].strip()

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Rewrite requires as a mapping: `requires:\n speckit_version: ">=1.0.0"`.
  2. Remove stray list dashes or quoting that turns the section into a string.
  3. Model it on a known-good bundled extension manifest (e.g. extensions/agent-context).

Example fix

# before
requires: ">=1.0.0"

# after
requires:
  speckit_version: ">=1.0.0"
Defensive patterns

Strategy: validation

Validate before calling

def requires_ok(data: dict) -> bool:
    return isinstance(data.get("requires"), dict)

Type guard

def is_requires_mapping(data: dict) -> bool:
    return isinstance(data.get("requires"), dict)

Prevention

When it happens

Trigger: A manifest has `requires: "spec-kit>=1.0"` or `requires: - speckit_version: '>=1.0'` (list of mappings). The isinstance check fails and ValidationError names the actual type.

Common situations: Author assumes requires is a list of requirement strings (pip-style) or a single constraint string, instead of a mapping keyed by speckit_version.

Related errors


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