dmtrKovalenko/fff · error · Error

expected a JSON object

Error message

expected a JSON object

What it means

The config file parsed as JSON but the top-level value is not a JSON object (e.g. an array, string, number, or null). FffConfig requires an object whose keys are known option names, so loadConfig rejects any other top-level shape with "expected a JSON object".

Solutions

  1. Wrap the settings in a top-level object: { "option": value, ... }.
  2. Check what wrote the file — fix the writer to serialize an object, not an array or scalar.
  3. Restore or recreate the config file with the documented schema (keys from CONFIG_KEYS).
  4. Add a schema check in your tooling that fails when the parsed root is not an object.

Example fix

// before (config.json)
[
  { "keymaps": { "open": "<leader>ff" } }
]
// after
{
  "keymaps": { "open": "<leader>ff" }
}
Defensive patterns

Strategy: validation

Validate before calling

const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
  throw new Error(`${configPath}: expected a JSON object at the top level`);
}

Type guard

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

Try / catch

try {
  cfg = config(configPath);
} catch (e) {
  if (/expected a JSON object/.test(e.message)) {
    console.error(`${configPath} must contain a { ... } object; using defaults`);
    cfg = defaultConfig;
  } else throw e;
}

Prevention

When it happens

Trigger: Config file containing just a JSON array ([...]), a bare string/number, or the literal null; a script that serialized the wrong value into the config path; concatenating config fragments into an array.

Common situations: Overwriting the config with a settings array from another tool; an export script doing JSON.stringify(items) into the config; a user writing "my settings" (a bare string) into the file by accident.

Related errors


AI-assisted analysis of dmtrKovalenko/fff@7f8537e70f (2026-09-10). Data as JSON: /api/errors/ec3b5d81433bb4d9. Report an issue: GitHub.

Appendix: source

Thrown at packages/pi-fff/src/config.ts:53

  try {
    contents = readFileSync(configPath, "utf8");
  } catch (error: unknown) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
    throw new Error(
      `Could not read pi-fff config at ${configPath}: ${errorMessage(error)}`,
    );
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(contents);
  } catch (error: unknown) {
    throw invalidConfig(configPath, `not valid JSON (${errorMessage(error)})`);
  }

  if (!isRecord(parsed)) {
    throw invalidConfig(configPath, "expected a JSON object");
  }

  for (const key of Object.keys(parsed)) {
    if (!CONFIG_KEYS.has(key as keyof FffConfig)) {
      throw invalidConfig(configPath, `unknown option "${key}"`);
    }
  }

  if (parsed.mode !== undefined && !VALID_MODES.includes(parsed.mode as FffMode)) {
    throw invalidConfig(configPath, `"mode" must be one of ${VALID_MODES.join(", ")}`);
  }

  validateString(configPath, parsed, "$schema");
  validateString(configPath, parsed, "frecencyDbPath");
  validateString(configPath, parsed, "historyDbPath");
  validateBoolean(configPath, parsed, "enableFsRootScanning");
  validateBoolean(configPath, parsed, "enableHomeDirScanning");
  validateBoolean(configPath, parsed, "warnOnHomeDirScan");

View on GitHub (pinned to 7f8537e70f)