nexu-io/open-design · error

${filePath} must contain a JSON object

Error message

${filePath} must contain a JSON object

What it means

Thrown by readJsonObject() after a successful JSON.parse when the parsed value is not a JSON object — i.e. it is an array, string, number, boolean, or null. The artifacts CLI --manifest flag must carry an object (the manifest fields), so any top-level non-object is rejected.

Source

Thrown at apps/daemon/src/artifacts-cli.ts:101

    } else {
      return { error: `unknown option: ${arg}` };
    }
  }

  return options;
}

async function readJsonObject(filePath: string): Promise<JsonObject> {
  const text = await readFile(filePath, 'utf8');
  let value: unknown;
  try {
    value = JSON.parse(text) as unknown;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`invalid JSON in ${filePath}: ${message}`);
  }
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error(`${filePath} must contain a JSON object`);
  }
  return value as JsonObject;
}

export async function runArtifactsCli(args: string[]): Promise<ArtifactCliResult> {
  const options = parseOptions(args);
  if ('error' in options) return fail(options.error);
  if (options.help || !options.command) {
    process.stdout.write(USAGE);
    return { exitCode: options.command ? 0 : 1 };
  }
  if (options.command !== 'create') return fail(`unknown artifacts command: ${options.command}`);
  if (!options.name) return fail('create requires --name <path>');
  if (!options.inputPath) return fail('create requires --input <file>');

  try {
    const daemonUrl = await resolveDaemonUrl(
      options.daemonUrl === undefined ? {} : { flagUrl: options.daemonUrl },

View on GitHub (pinned to 5be4028344)

Solutions

  1. Ensure the manifest file's top-level value is a single JSON object delimited by { }.
  2. If you have multiple manifests, pick one per invocation — the CLI takes one artifact at a time.
  3. Run `node -e "const v=JSON.parse(require('fs').readFileSync('<path>','utf8')); console.log(typeof v, Array.isArray(v))"` to confirm it parses to a non-array object.

Example fix

// before: manifest.json (array)
[ { "kind": "html", "renderer": "html", "exports": ["html"] } ]

// after: single object
{ "kind": "html", "renderer": "html", "exports": ["html"] }
Defensive patterns

Strategy: type-guard

Validate before calling

import { readFileSync } from 'node:fs';
const parsed = JSON.parse(readFileSync(p, 'utf8'));
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
  throw new Error(`${p} must contain a JSON object`);
}

Type guard

function isPlainJsonObject(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

Prevention

When it happens

Trigger: Passing --manifest a file whose contents are a JSON array `[...]`, a bare string `"..."`, a number, or `null`. Also triggered by a file that contains only `[]`.

Common situations: Author confused a manifest object with a list of manifests; exported a JSON array from a tool; truncated a file down to a scalar; passed a package.json-style top-level array by mistake.

Related errors


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