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
- Wrap the top-level value in an object: change `[ ... ]` to `{ "items": [ ... ] }` or the expected object shape.
- Replace `null`/scalar contents with the required object structure for the label shown in the message.
- Regenerate the file with the tooling that produced the correct object schema.
- 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
- Match the documented schema: top-level object, never a bare array or scalar.
- Add a shape assertion in tests for every config file the tooling reads.
- Regenerate files with the official scaffolder instead of hand-writing structure.
- Validate parsed JSON shape at ingestion boundaries in your own scripts.
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
- Canonical session snapshot must be an object
- Canonical session snapshot requires
- Canonical session snapshot requires
- Canonical session snapshot requires
- Expected a JSON object
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)