actualbudget/actual · warning
Invalid catalog format: expected an array
Error message
Invalid catalog format: expected an array
What it means
After a successful fetch, fetchCatalog validates that the parsed JSON body is an array. If the endpoint returned valid JSON of another shape (object, null, string), it throws 'Invalid catalog format: expected an array'. This protects the theme picker from runtime crashes when rendering an unexpected payload.
Source
Thrown at packages/desktop-client/src/hooks/useThemeCatalog.ts:31
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchCatalog = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(CATALOG_URL);
if (!response.ok) {
throw new Error(`Failed to fetch catalog: ${response.statusText}`);
}
const data = await response.json();
// Validate that data is an array
if (!Array.isArray(data)) {
throw new Error('Invalid catalog format: expected an array');
}
setData(data);
} catch (err) {
setError(
err instanceof Error ? err.message : 'Failed to load theme catalog',
);
} finally {
setIsLoading(false);
}
};
void fetchCatalog();
}, []);
return {
data,
isLoading,View on GitHub (pinned to d4334cb6e6)
Solutions
- Verify the endpoint actually returns a top-level JSON array (curl the URL and inspect the body)
- If the API changed, update fetchCatalog to unwrap the envelope (e.g. data.themes)
- Harden the code: also validate the shape of individual catalog items before setData
- Point CATALOG_URL to the correct, current catalog URL
Example fix
// before
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('Invalid catalog format: expected an array');
}
// after
const data = await response.json();
const themes = Array.isArray(data) ? data : Array.isArray(data?.themes) ? data.themes : null;
if (!themes) {
throw new Error('Invalid catalog format: expected an array');
}
setData(themes); Defensive patterns
Strategy: validation
Validate before calling
const isThemeArray = (data: unknown): data is ThemeCatalogItem[] => Array.isArray(data) && data.every(t => !!t && typeof t === 'object' && typeof (t as any).name === 'string');
Type guard
const isThemeCatalog = (x: unknown): x is ThemeCatalogItem[] =>
Array.isArray(x) && x.every(item =>
typeof item === 'object' && item !== null &&
typeof (item as any).repo === 'string' && typeof (item as any).name === 'string'
); Try / catch
const { catalog, error } = useThemeCatalog();
if (error?.includes('Invalid catalog format')) {
// surface a schema-mismatch warning and use bundled themes only
console.error('Catalog schema changed:', error);
} Prevention
- Contract-test the catalog endpoint shape in CI with a fetch + schema assertion
- Keep a versioned schema (e.g. { version, themes }) and handle both shapes during migration
- Never trust remote JSON: validate items before rendering
- Log the received payload type when validation fails to speed up diagnosis
When it happens
Trigger: CATALOG_URL serves JSON that parses but is not an array — e.g. an object like { themes: [...] }, an HTML error page parsed as text, a null response, or an API version change that wrapped the array.
Common situations: The catalog endpoint was updated to a paginated/envelope response format; a proxy or captive portal returned a JSON error body; misconfigured CDN serving a stub file.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Invalid config file: expected an object with keys: ${configF
- Field "${field}" does not exist on table ${table}: ${JSON.st
- "${field}" is required for table "${table}": ${JSON.stringif
- Invalid dashboard.widgets data type: it must be an array of
- validation-error
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/a71dc57c29a72233.
Report an issue: GitHub.