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
- Quote the name so it is a string: "name": "plan".
- Coerce generated names with str() and skip/null-check before serialization.
- 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
- Quote names in YAML to avoid int/bool parsing.
- Coerce with str() in generators; reject None names.
- Reuse slug names identical to command slugs' style.
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
- Invalid command name: expected a string, got {type(cmd['name
- Aliases for command '{cmd['name']}' must be strings
- Invalid {singular} description for '{name}': expected a stri
- Invalid runtimes for script '{name}': expected a list of str
- Manifest must be a YAML mapping, got {type(data).__name__}:
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/a6440969ca8a3f78.
Report an issue: GitHub.