affaan-m/ECC · error

Invalid at : expected a JSON object

Error message

Invalid ${label} at ${filePath}: expected a JSON object

What it means

After readJsonObject successfully parses a JSON file, it validates the shape: the value must be a non-null object and not an array. If the file contains valid JSON whose top level is an array, a string, a number, a boolean, or null, this error is thrown. It distinguishes 'parsed but wrong shape' from 'unparseable' (error 363).

Solutions

  1. Wrap the top-level value in an object: change `[ ... ]` to `{ "items": [ ... ] }` or the expected object shape.
  2. Replace `null`/scalar contents with the required object structure for the label shown in the message.
  3. Regenerate the file with the tooling that produced the correct object schema.
  4. Check documentation for the expected schema of the labeled file (e.g. scaffold/settings JSON object).

Example fix

// before: top-level array
[
  { "id": "a" },
  { "id": "b" }
]

// after: top-level object
{
  "components": [
    { "id": "a" },
    { "id": "b" }
  ]
}
Defensive patterns

Strategy: type-guard

Validate before calling

const v = JSON.parse(fs.readFileSync(p, 'utf8'));
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
  throw new Error(`${p} must contain a top-level JSON object`);
}

Type guard

function isJsonObject(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

Try / catch

try {
  const cfg = readJsonObject(path, label);
} catch (e) {
  if (e.message.includes('expected a JSON object')) {
    console.error(`${path} has wrong top-level shape: wrap arrays/scalars in an object.`);
    return regenerateFile(path);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling readJsonObject(filePath, label) when the file parses but its root value is `null`, an array (e.g. a top-level `[ ... ]` list), or a JSON scalar such as `"text"`, `42`, or `true` instead of an object.

Common situations: A scaffold file exported as a JSON array of items; a config file containing just `null` or an empty file's fallback; hand-written config using a bare string; tooling that serializes an array of components where an object map was expected.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/0bd6f4d5cd4fbcce. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install/plan.js:128

    sourceRelativePath,
    destinationPath,
    strategy,
    ownership: 'managed',
    scaffoldOnly: false,
    ...(contentTransform ? { contentTransform } : {}),
  };
}

function readJsonObject(filePath, label) {
  let parsed;
  try {
    parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
  } catch (error) {
    throw new Error(`Failed to parse ${label} at ${filePath}: ${error.message}`);
  }

  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error(`Invalid ${label} at ${filePath}: expected a JSON object`);
  }

  return parsed;
}

function materializeClaudeSettingsOperation(sourceRoot, operation) {
  const sourcePath = path.join(sourceRoot, operation.sourceRelativePath);
  if (!fs.existsSync(sourcePath)) {
    return [];
  }

  // Stable ids and descriptions live in hooks/hooks.metadata.json; readHooksConfig
  // merges them back so managed settings entries keep their ids.
  const hooksConfig = readHooksConfig(sourcePath, operation.sourceRelativePath);
  const managedHooks = materializeManagedHooks(
    hooksConfig,
    path.dirname(operation.destinationPath)
  );

View on GitHub (pinned to 8321021c54)