github/spec-kit · error · ExtensionError

Invalid catalog format from {url}: 'extensions' must be a JS

Error message

Invalid catalog format from {url}: 'extensions' must be a JSON object

What it means

Third stage of catalog validation: schema_version is present and extensions is present, but extensions is not a JSON object (typically a list). The catalog indexes extensions by id as object keys, so an array form is rejected with the ''extensions' must be a JSON object' variant.

Source

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

        surfaces as the user-facing ``Invalid catalog format`` error
        instead of a raw Python traceback.

        Args:
            catalog_data: Parsed JSON payload from the catalog source.
            url: Source URL — used in the error message so the user can
                tell which catalog in a multi-catalog stack is malformed.

        Raises:
            ExtensionError: If the payload's shape is invalid.
        """
        if not isinstance(catalog_data, dict):
            raise ExtensionError(
                f"Invalid catalog format from {url}: expected a JSON object"
            )
        if "schema_version" not in catalog_data or "extensions" not in catalog_data:
            raise ExtensionError(f"Invalid catalog format from {url}")
        if not isinstance(catalog_data.get("extensions"), dict):
            raise ExtensionError(
                f"Invalid catalog format from {url}: 'extensions' must be a JSON object"
            )

    def get_active_catalogs(self) -> List[CatalogEntry]:
        """Get the ordered list of active catalogs.

        Resolution order:
        1. SPECKIT_CATALOG_URL env var — single catalog replacing all defaults
        2. Project-level .specify/extension-catalogs.yml
        3. User-level ~/.specify/extension-catalogs.yml
        4. Built-in default stack (default + community)

        Returns:
            List of CatalogEntry objects sorted by priority (ascending)

        Raises:
            ValidationError: If a catalog URL is invalid
        """

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Convert the array to an object keyed by extension id: {"schema_version":1,"extensions":{"x":{"id":"x",...}}}
  2. Verify with json.tool that the value after 'extensions:' starts with { not [
  3. Publish the corrected file and refresh any caches (re-run the fetch or clear the cached catalog metadata)

Example fix

# before
{
  "schema_version": 1,
  "extensions": [
    {"id": "my-ext", "name": "My Ext"}
  ]
}

# after
{
  "schema_version": 1,
  "extensions": {
    "my-ext": {"id": "my-ext", "name": "My Ext"}
  }
}
Defensive patterns

Strategy: validation

Validate before calling

def extensions_is_object(data: dict) -> bool:
    return isinstance(data.get('extensions'), dict)

Type guard

def is_catalog_shaped(data: object) -> bool:
    return (
        isinstance(data, dict)
        and isinstance(data.get('schema_version'), (int, str))
        and isinstance(data.get('extensions'), dict)
    )

Prevention

When it happens

Trigger: A catalog document like {"schema_version":1,"extensions":[{"id":"x",...}]} — the extensions value is an array, string, or number instead of an id-keyed object.

Common situations: Converting an API's list response into a catalog by just wrapping it; authors assuming array form because that's how search results are returned (search() returns a list); copy-paste from _get_merged_extensions output which is list-shaped.

Related errors


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