{"record":{"id":"cf4511c832269434","repo":"github/spec-kit","slug":"invalid-extension-field-expected-a-string-got","errorCode":null,"errorMessage":"Invalid extension.{field}: expected a string, got {type(ext[field]).__name__}","messagePattern":"Invalid extension\\.(.+?): expected a string, got (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":310,"sourceCode":"            raise ValidationError(\n                f\"Invalid extension: expected a mapping, got {type(ext).__name__}\"\n            )\n        # Check presence AND type: the format/version checks below feed these\n        # values straight to ``re.match`` and ``packaging.Version``, both of\n        # which raise a bare TypeError on a non-string. YAML makes that an easy\n        # authoring slip -- unquoted ``version: 1.0`` parses as a float and\n        # ``id: 2`` as an int -- and TypeError is not a ValidationError, so it\n        # escapes every caller that already handles a malformed manifest (see\n        # list_installed()'s \"Corrupted extension\" fallback, which catches\n        # ValidationError only, making one bad extension exit ``specify\n        # extension list`` with a raw traceback and hide the healthy ones).\n        # Mirrors the sibling IntegrationDescriptor, which already type-checks\n        # the same four fields.\n        for field in [\"id\", \"name\", \"version\", \"description\"]:\n            if field not in ext:\n                raise ValidationError(f\"Missing extension.{field}\")\n            if not isinstance(ext[field], str):\n                raise ValidationError(\n                    f\"Invalid extension.{field}: expected a string, \"\n                    f\"got {type(ext[field]).__name__}\"\n                )\n\n        # Validate extension ID format\n        if not re.match(r\"^[a-z0-9-]+$\", ext[\"id\"]):\n            raise ValidationError(\n                f\"Invalid extension ID '{ext['id']}': \"\n                \"must be lowercase alphanumeric with hyphens only\"\n            )\n\n        # Validate semantic version\n        try:\n            pkg_version.Version(ext[\"version\"])\n        except pkg_version.InvalidVersion:\n            raise ValidationError(f\"Invalid version: {ext['version']}\")\n\n        # Validate optional category field (free-form string)","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L292-L328","documentation":"Raised when one of the four extension metadata fields (id, name, version, description) is present but not a string. The guard exists because the values are later fed to re.match and packaging.Version, which raise bare TypeError on non-strings — and unquoted YAML makes that an easy slip (`version: 1.0` parses as float, `id: 2` as int). TypeError is not a ValidationError, so without this guard it escaped every malformed-manifest handler, including list_installed()'s 'Corrupted extension' fallback, crashing `specify extension list` for all extensions.","triggerScenarios":"Manifest contains unquoted `version: 1.0` (YAML float), `id: 2` (int), or any field given as a list/bool; the isinstance(ext[field], str) check fails and names the actual type.","commonSituations":"Forgetting quotes on version — the classic case, since semvers look like numbers; numeric ids in examples; YAML boolean coercion for `name: no`. Mirrors the same four-field type check in IntegrationDescriptor.","solutions":["Quote the offending value so YAML yields a string, e.g. `version: \"1.0.0\"` and `id: \"ext-2\"`.","Read the type name in the message: 'float' almost always means unquoted version.","Re-validate with specify extension list/install."],"exampleFix":"# before\nextension:\n  id: my-ext\n  version: 1.0.0   # parses as float? no - but 1.0 does\n\n# after\nextension:\n  id: my-ext\n  version: \"1.0.0\"","handlingStrategy":"type-guard","validationCode":"for f in [\"id\", \"name\", \"version\", \"description\"]:\n    if not isinstance(data[\"extension\"].get(f), str):\n        data[\"extension\"][f] = str(data[\"extension\"][f])  # or fail fast","typeGuard":"def extension_fields_are_strings(ext: dict) -> bool:\n    return all(isinstance(ext.get(f), str) for f in (\"id\", \"name\", \"version\", \"description\"))","tryCatchPattern":"except ValidationError as e:\n    if \"expected a string\" in str(e):\n        quote_yaml_scalars(path)  # re-save with version: \"1.0.0\" style quoting","preventionTips":["Always quote version (and any numeric-looking value) in YAML.","Remember YAML type coercion: 1.0 -> float, 2 -> int, no -> bool; manifests want strings."],"tags":["manifest","validation","yaml","type-coercion","extensions"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}