nexu-io/open-design · error · Error

brand is not a JSON object

Error message

brand is not a JSON object

What it means

Thrown by validateBrand when the input is not a JSON object — null, an array, a primitive (string/number/bool), or undefined. validateBrand normalizes agent-produced or hand-authored brand JSON, and the very first invariant is that it must be an object. Earlier extraction usually runs the input through extractJsonBlock, which can yield null/array output.

Source

Thrown at apps/daemon/src/brands/validate.ts:52

function fontSpec(raw: unknown, fallbackFamily: string): BrandFontSpec {
  const o = (raw ?? {}) as Record<string, unknown>;
  return {
    family: isStr(o.family) && o.family ? o.family : fallbackFamily,
    fallbacks: strArr(o.fallbacks),
    weights: Array.isArray(o.weights) ? o.weights.filter((w) => typeof w === 'number') : [400, 700],
    ...(isStr(o.googleFontsUrl) && o.googleFontsUrl ? { googleFontsUrl: o.googleFontsUrl } : {}),
    ...(isStr(o.notes) && o.notes ? { notes: o.notes } : {}),
  };
}

/**
 * Validate + normalize a brand object. Throws with a precise message on
 * unrecoverable problems (missing name/colors); fills sensible defaults for
 * everything optional so a slightly-sloppy input still renders.
 */
export function validateBrand(raw: unknown, sourceUrl: string): Brand {
  if (!raw || typeof raw !== 'object') throw new Error('brand is not a JSON object');
  const o = raw as Record<string, unknown>;

  if (!isStr(o.name) || !o.name.trim()) throw new Error('brand: missing required `name`');

  const rawColors = Array.isArray(o.colors) ? o.colors : [];
  const colors: BrandColor[] = [];
  for (const c of rawColors) {
    if (!c || typeof c !== 'object') continue;
    const co = c as Record<string, unknown>;
    const role = BRAND_COLOR_ROLES.includes(co.role as BrandColorRole)
      ? (co.role as BrandColorRole)
      : null;
    const hex = isStr(co.hex) && /^#[0-9a-fA-F]{6}$/.test(co.hex) ? co.hex.toLowerCase() : null;
    if (!role || !hex) continue;
    colors.push({
      role,
      hex,
      oklch: isStr(co.oklch) ? co.oklch : '',

View on GitHub (pinned to 5be4028344)

Solutions

  1. Pre-check the parsed value with `typeof raw === 'object' && raw !== null && !Array.isArray(raw)` before validateBrand.
  2. Improve the extraction prompt to require a single JSON object, not a list, and to omit null.
  3. If extraction yields an array, take the first element only after validating it is an object.
  4. On validateBrand failure, re-prompt the agent with the error message rather than crashing the extraction.

Example fix

// before
const brand = validateBrand(parsed, sourceUrl);
// after
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
  throw new Error('expected a brand JSON object from the extractor');
}
const brand = validateBrand(parsed, sourceUrl);
Defensive patterns

Strategy: type-guard

Validate before calling

if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
  throw new Error('expected a brand JSON object from the extractor');
}

Type guard

const isPlainObject = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null && !Array.isArray(v);

Prevention

When it happens

Trigger: validateBrand(parsed, sourceUrl) where parsed came from JSON.parse of an LLM-produced text block that yielded an array ('[{...}]'), the string 'null', a bare primitive, or null because no JSON block was found.

Common situations: LLM wraps the brand in a list '[...]' or returns the literal 'null'; agent emits prose instead of JSON and extractJsonBlock falls back to grabbing text between { and } that is not an object; a hand-authored brand.json file containing a top-level array; an empty file.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/3d5cb9313dbc8b0b. Report an issue: GitHub.