{"record":{"id":"a9db17755b1e959a","repo":"paperclipai/paperclip","slug":"expected-a-json-object-path-a9db17","errorCode":null,"errorMessage":"Expected a JSON object: ${path}","messagePattern":"Expected a JSON object: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs","lineNumber":38,"sourceCode":"\nconst SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;\nconst ATTEMPT_FILES = new Set([\n  \"request.json\",\n  \"artifact.json\",\n  \"score.json\",\n  \"compiled-fixture.json\",\n  \"case.json\",\n  \"config.json\",\n]);\n\nfunction json(value) {\n  return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nasync function loadObject(path) {\n  const value = JSON.parse(await readFile(path, \"utf8\"));\n  if (value === null || Array.isArray(value) || typeof value !== \"object\") {\n    throw new Error(`Expected a JSON object: ${path}`);\n  }\n  return value;\n}\n\nfunction safeId(value, label = \"identifier\") {\n  const result = String(value ?? \"\");\n  if (!SAFE_ID.test(result)) throw new Error(`Unsafe ${label}: ${result}`);\n  return result;\n}\n\nfunction inside(root, candidate, label) {\n  const rel = relative(resolve(root), resolve(candidate));\n  if (!rel || rel === \"..\" || rel.startsWith(`..${sep}`)) {\n    throw new Error(`${label} escapes its declared root`);\n  }\n  return resolve(candidate);\n}\n","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs#L20-L56","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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).","solutions":["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).","Verify you are pointing at the intended file, not a list-valued JSON document.","If the data is genuinely a list, iterate and call loadObject per item on individual files instead."],"exampleFix":"// before: rosters.json contains\n// [ { \"id\": \"r1\", ... } ]\n// after: each roster file contains\n// { \"schema\": \"paperclip-runner/live-roster/v1\", \"id\": \"r1\", \"cases\": [ ... ] }","handlingStrategy":"validation","validationCode":"const value = JSON.parse(await readFile(path, \"utf8\"));\nif (value === null || Array.isArray(value) || typeof value !== \"object\") {\n  throw new Error(`Expected a JSON object: ${path}`);\n}","typeGuard":"const isJsonObject = (v) => v !== null && !Array.isArray(v) && typeof v === \"object\";","tryCatchPattern":"try {\n  campaign = await loadObject(path);\n} catch (err) {\n  if (err.message.startsWith(\"Expected a JSON object\")) console.error(`Fix the top-level shape of ${path}: must be { ... }, not an array/primitive.`);\n  throw err;\n}","preventionTips":["Validate campaign/roster JSON with a schema validator (e.g. zod) at load time.","Never hand-edit JSON root shape; use object wrappers with explicit schema fields.","Add a JSON lint/CI check that asserts every campaign/roster file root is an object."],"tags":["json","validation","shape"],"backgroundTag":"unexpected-response-shape","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}