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
- Convert the array to an object keyed by extension id: {"schema_version":1,"extensions":{"x":{"id":"x",...}}}
- Verify with json.tool that the value after 'extensions:' starts with { not [
- 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
- Key the extensions mapping by extension id; never publish array form
- Remember search() output is list-shaped — don't round-trip it into a catalog
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
- Invalid catalog format from {url}: expected a JSON object
- Invalid catalog format from {url}
- Invalid JSON in catalog from {entry.url}: {e}
- Invalid JSON in catalog: {e}
- Invalid catalog format from {entry.url}: {shape_error}
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/0341b9a43238adb7.
Report an issue: GitHub.