paperclipai/paperclip · error · Error

Expected a JSON object: ${path}

Error message

Expected a JSON object: ${path}

What it means

loadObject parses a JSON file and requires the top-level value to be a plain object (not null, not an array, not a primitive). It is used to load campaign and roster files for the runner protocol eval campaign. Any JSON document whose root is not an object is rejected with the offending path in the message.

Source

Thrown at packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs:38

const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
const ATTEMPT_FILES = new Set([
  "request.json",
  "artifact.json",
  "score.json",
  "compiled-fixture.json",
  "case.json",
  "config.json",
]);

function json(value) {
  return `${JSON.stringify(value, null, 2)}\n`;
}

async function loadObject(path) {
  const value = JSON.parse(await readFile(path, "utf8"));
  if (value === null || Array.isArray(value) || typeof value !== "object") {
    throw new Error(`Expected a JSON object: ${path}`);
  }
  return value;
}

function safeId(value, label = "identifier") {
  const result = String(value ?? "");
  if (!SAFE_ID.test(result)) throw new Error(`Unsafe ${label}: ${result}`);
  return result;
}

function inside(root, candidate, label) {
  const rel = relative(resolve(root), resolve(candidate));
  if (!rel || rel === ".." || rel.startsWith(`..${sep}`)) {
    throw new Error(`${label} escapes its declared root`);
  }
  return resolve(candidate);
}

View on GitHub (pinned to 01ad858492)

Solutions

  1. Open the file at the path in the message and wrap the top-level value in an object with the expected fields (schema, id, cases/lanes).
  2. Verify you are pointing at the intended file, not a list-valued JSON document.
  3. If the data is genuinely a list, iterate and call loadObject per item on individual files instead.

Example fix

// before: rosters.json contains
// [ { "id": "r1", ... } ]
// after: each roster file contains
// { "schema": "paperclip-runner/live-roster/v1", "id": "r1", "cases": [ ... ] }
Defensive patterns

Strategy: validation

Validate before calling

const value = JSON.parse(await readFile(path, "utf8"));
if (value === null || Array.isArray(value) || typeof value !== "object") {
  throw new Error(`Expected a JSON object: ${path}`);
}

Type guard

const isJsonObject = (v) => v !== null && !Array.isArray(v) && typeof v === "object";

Try / catch

try {
  campaign = await loadObject(path);
} catch (err) {
  if (err.message.startsWith("Expected a JSON object")) console.error(`Fix the top-level shape of ${path}: must be { ... }, not an array/primitive.`);
  throw err;
}

Prevention

When it happens

Trigger: Calling loadObject(path) on a file whose JSON.parse result is null, an array, a string, a number, or a boolean — e.g. a roster file starting with '[' or a file containing just 'null'.

Common situations: A roster/campaign JSON was hand-edited into an array form; an export tool wrote a JSON list; a file contains only 'null'; wrong file passed (e.g. a JSON list of rosters instead of a single roster object).

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/a9db17755b1e959a. Report an issue: GitHub.