github/spec-kit · error · ValidationError

Invalid {singular} name: expected a string, got {type(name).

Error message

Invalid {singular} name: expected a string, got {type(name).__name__}

What it means

The 'name' field of a provides.templates/scripts entry is not a string (int, bool, null, dict...). The explicit isinstance guard runs before the VALID_EXTENSION_ARTIFACT_NAME_PATTERN regex, mirroring the command-name guard: without it a non-string would escape as a TypeError from re.match instead of ValidationError.

Source

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

        who copies a preset-style entry gets a clear error instead of a
        silently-dropped field. Duplicate names within a section are also
        rejected: the resolver returns the first matching entry by name
        (``PresetResolver._extension_manifest_declared_template``), so a
        later duplicate would be silently unreachable while still being
        exposed by ``ExtensionManifest.templates``/``.scripts``.
        """
        seen_names: set[str] = set()
        for entry in entries:
            if not isinstance(entry, dict):
                raise ValidationError(
                    f"Each entry in 'provides.{section}' must be a mapping"
                )
            if "name" not in entry or "file" not in entry:
                raise ValidationError(f"{singular.capitalize()} missing 'name' or 'file'")

            name = entry["name"]
            if not isinstance(name, str):
                raise ValidationError(
                    f"Invalid {singular} name: expected a string, got {type(name).__name__}"
                )
            if not VALID_EXTENSION_ARTIFACT_NAME_PATTERN.match(name):
                raise ValidationError(
                    f"Invalid {singular} name '{name}': "
                    "must be lowercase alphanumeric with hyphens only"
                )
            if name in seen_names:
                raise ValidationError(
                    f"Duplicate {singular} name '{name}' in 'provides.{section}'"
                )
            seen_names.add(name)

            file_value = entry["file"]
            reason = relative_extension_path_violation(file_value)
            if reason:
                label = repr(file_value) if isinstance(file_value, str) else f"for {singular} '{name}'"
                raise ValidationError(f"Invalid {singular} 'file' {label}: {reason}")

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Quote the name so it is a string: "name": "plan".
  2. Coerce generated names with str() and skip/null-check before serialization.
  3. Re-install the extension after fixing the manifest.

Example fix

// before
{ "name": 2, "file": "templates/plan.md" }
// after
{ "name": "plan-2", "file": "templates/plan.md" }
Defensive patterns

Strategy: type-guard

Validate before calling

for section in ("templates", "scripts"):
    for entry in manifest.get("provides", {}).get(section, []):
        if isinstance(entry, dict) and "name" in entry:
            assert isinstance(entry["name"], str), f"non-string name: {entry['name']!r}"

Type guard

def is_str_artifact_name(entry: dict) -> bool:
    return isinstance(entry.get("name"), str)

Try / catch

try:
    ExtensionManifest.load(path)
except ValidationError as e:
    if "expected a string" in str(e):
        # quote/coerce the artifact name and retry
        ...

Prevention

When it happens

Trigger: {"name": 2, "file": "..."}, {"name": true, ...}, or YAML name: 42 unquoted. Raised in the artifact-name check during manifest validation.

Common situations: Unquoted numeric names in YAML; programmatic manifest builders inserting an int id; optional-name code paths emitting null.

Related errors


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