actualbudget/actual · error · Error
Query file must contain a JSON object
Error message
Query file must contain a JSON object
What it means
When `actual query --file <path>` (or piped JSON via stdin) is used, the CLI parses the input as JSON with readJsonInput and requires the top-level value to be a JSON object (the AQL query itself). A top-level array, string, number, or null is rejected with this error by registerQueryCommand before any query executes.
Source
Thrown at packages/cli/src/commands/query.ts:309
)
.option('--count', 'Count matching rows instead of returning them')
.option(
'--group-by <fields>',
'Comma-separated fields to group by (use with aggregate selects)',
)
.option(
'--file <path>',
'Read full query object from JSON file (use - for stdin)',
)
.addHelpText('after', RUN_EXAMPLES)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(
opts,
async () => {
const parsed = cmdOpts.file ? readJsonInput(cmdOpts) : undefined;
if (parsed !== undefined && !isRecord(parsed)) {
throw new Error('Query file must contain a JSON object');
}
const queryObj = parsed
? buildQueryFromFile(parsed, cmdOpts.table)
: buildQueryFromFlags(cmdOpts);
const result = await api.aqlQuery(queryObj);
if (!isRecord(result) || !('data' in result)) {
throw new Error('Query result missing data');
}
if (cmdOpts.count) {
printOutput({ count: result.data }, opts.format);
} else {
printOutput(result.data, opts.format);
}
},
{ mutates: false },View on GitHub (pinned to d4334cb6e6)
Solutions
- Ensure the file's top level is a single JSON object: { "table": "transactions", "select": [...] }
- If you have an array, index into it or unwrap it (e.g. jq '.[0]') before passing
- Validate the file: `jq type file.json` must print "object"
- Check the file is not empty or truncated
Example fix
// before (query.json)
[
{ "table": "transactions", "select": ["date"] }
]
// after
{
"table": "transactions",
"select": ["date"]
} Defensive patterns
Strategy: type-guard
Validate before calling
const raw = JSON.parse(readFileSync(queryFile, 'utf8'));
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
throw new Error(`${queryFile} must contain a single JSON object (the AQL query)`);
} Type guard
function isQueryObject(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
} Try / catch
try {
await run(['actual', 'query', '--file', queryFile]);
} catch (e) {
if (String(e.message).includes('must contain a JSON object')) {
console.error(`Unwrap arrays/scalars in ${queryFile}: top level must be {"table":..., "select":...}`);
} else throw e;
} Prevention
- Validate query files with `jq type file.json` — expect "object"
- Store one query per file; keep result dumps in separate files from queries
- When generating query files programmatically, assert the serialized top level is an object
When it happens
Trigger: Passing a file whose contents are a JSON array like `[{...}]`; piping a bare JSON array or scalar into `actual query --file -`; a file containing `null`; a file that stores a list of queries rather than one query object.
Common situations: Exporting query logs that wrap the query in an array; hand-editing a query file and accidentally changing the outer braces to brackets; tools like jq re-serializing output that was itself an array of results.
Related errors
- --where and --filter are mutually exclusive
- --count and --select are mutually exclusive
- Query result missing data
- Unknown table "${table}". Available tables: ${Object.keys(TA
- Invalid config file: expected an object with keys: ${configF
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/23e90d61c24b1d15.
Report an issue: GitHub.