{"record":{"id":"0341b9a43238adb7","repo":"github/spec-kit","slug":"invalid-catalog-format-from-url-extensions-mu","errorCode":null,"errorMessage":"Invalid catalog format from {url}: 'extensions' must be a JSON object","messagePattern":"Invalid catalog format from (.+?): 'extensions' must be a JSON object","errorType":"exception","errorClass":"ExtensionError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":3696,"sourceCode":"        surfaces as the user-facing ``Invalid catalog format`` error\n        instead of a raw Python traceback.\n\n        Args:\n            catalog_data: Parsed JSON payload from the catalog source.\n            url: Source URL — used in the error message so the user can\n                tell which catalog in a multi-catalog stack is malformed.\n\n        Raises:\n            ExtensionError: If the payload's shape is invalid.\n        \"\"\"\n        if not isinstance(catalog_data, dict):\n            raise ExtensionError(\n                f\"Invalid catalog format from {url}: expected a JSON object\"\n            )\n        if \"schema_version\" not in catalog_data or \"extensions\" not in catalog_data:\n            raise ExtensionError(f\"Invalid catalog format from {url}\")\n        if not isinstance(catalog_data.get(\"extensions\"), dict):\n            raise ExtensionError(\n                f\"Invalid catalog format from {url}: 'extensions' must be a JSON object\"\n            )\n\n    def get_active_catalogs(self) -> List[CatalogEntry]:\n        \"\"\"Get the ordered list of active catalogs.\n\n        Resolution order:\n        1. SPECKIT_CATALOG_URL env var — single catalog replacing all defaults\n        2. Project-level .specify/extension-catalogs.yml\n        3. User-level ~/.specify/extension-catalogs.yml\n        4. Built-in default stack (default + community)\n\n        Returns:\n            List of CatalogEntry objects sorted by priority (ascending)\n\n        Raises:\n            ValidationError: If a catalog URL is invalid\n        \"\"\"","sourceCodeStart":3678,"sourceCodeEnd":3714,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L3678-L3714","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"# before\n{\n  \"schema_version\": 1,\n  \"extensions\": [\n    {\"id\": \"my-ext\", \"name\": \"My Ext\"}\n  ]\n}\n\n# after\n{\n  \"schema_version\": 1,\n  \"extensions\": {\n    \"my-ext\": {\"id\": \"my-ext\", \"name\": \"My Ext\"}\n  }\n}","handlingStrategy":"validation","validationCode":"def extensions_is_object(data: dict) -> bool:\n    return isinstance(data.get('extensions'), dict)","typeGuard":"def is_catalog_shaped(data: object) -> bool:\n    return (\n        isinstance(data, dict)\n        and isinstance(data.get('schema_version'), (int, str))\n        and isinstance(data.get('extensions'), dict)\n    )","tryCatchPattern":null,"preventionTips":["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"],"tags":["extensions","catalog","json","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}