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

  1. Ensure the file's top level is a single JSON object: { "table": "transactions", "select": [...] }
  2. If you have an array, index into it or unwrap it (e.g. jq '.[0]') before passing
  3. Validate the file: `jq type file.json` must print "object"
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/23e90d61c24b1d15. Report an issue: GitHub.