{"record":{"id":"cdbaa344c048b0b0","repo":"heygen-com/hyperframes","slug":"chain-file-must-be-a-json-object","errorCode":null,"errorMessage":"Chain file must be a JSON object.","messagePattern":"Chain file must be a JSON object\\.","errorType":"validation","errorClass":"AudioFxChainError","httpStatus":null,"severity":"error","filePath":"packages/core/src/audioFx.ts","lineNumber":768,"sourceCode":"    super(message);\n    this.name = \"AudioFxChainError\";\n  }\n}\n\n/**\n * Parse a chain file. Unknown effect ids are rejected rather than skipped: a\n * chain that silently loses a node would render differently from the project\n * the author saved, which is worse than refusing to render at all.\n */\nexport function parseAudioFxChain(json: string): HfAudioFxChain {\n  let raw: unknown;\n  try {\n    raw = JSON.parse(json);\n  } catch (err) {\n    throw new AudioFxChainError(`Chain file is not valid JSON: ${(err as Error).message}`);\n  }\n  if (typeof raw !== \"object\" || raw === null) {\n    throw new AudioFxChainError(\"Chain file must be a JSON object.\");\n  }\n  const obj = raw as { version?: unknown; nodes?: unknown };\n  if (obj.version !== HF_AUDIO_FX_CHAIN_VERSION) {\n    throw new AudioFxChainError(`Unsupported chain version: ${String(obj.version)}`);\n  }\n  if (!Array.isArray(obj.nodes)) {\n    throw new AudioFxChainError(\"Chain file is missing a `nodes` array.\");\n  }\n  const nodes: HfAudioFxNode[] = obj.nodes.map((n, i) => {\n    if (typeof n !== \"object\" || n === null) {\n      throw new AudioFxChainError(`Node ${i} is not an object.`);\n    }\n    const node = n as { type?: unknown; enabled?: unknown; params?: unknown };\n    if (typeof node.type !== \"string\" || !BY_ID.has(node.type)) {\n      throw new AudioFxChainError(`Node ${i} has unknown effect type: ${String(node.type)}`);\n    }\n    return {\n      type: node.type,","sourceCodeStart":750,"sourceCodeEnd":786,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/core/src/audioFx.ts#L750-L786","documentation":"After JSON.parse succeeds, parseAudioFxChain() requires the top-level value to be a JSON object (typeof === 'object' && non-null). A JSON array, number, string, boolean, or null at the top level is rejected because the chain schema is { version, nodes }. This guards against a syntactically valid but structurally wrong chain file.","triggerScenarios":"The chain string parses as a bare scalar or array — e.g. `[]`, `[{...nodes...}]` (wrapping nodes in an array instead of an object), `\"hello\"`, `42`, `null`, or `true`.","commonSituations":"Author writes the nodes array directly without the enclosing { version, nodes } object; a tool serializes just the nodes array; confusion between the chain object and its nodes field.","solutions":["Wrap the content in an object literal: { \"version\": 1, \"nodes\": [ ... ] }.","Regenerate with serializeAudioFxChain to guarantee the correct top-level shape."],"exampleFix":"// before\nparseAudioFxChain(JSON.stringify([{ type: \"highpass\" }])); // array at top level\n\n// after\nparseAudioFxChain(JSON.stringify({ version: 1, nodes: [{ type: \"highpass\" }] }));","handlingStrategy":"validation","validationCode":"function looksLikeChainObject(s: string): boolean {\n  try { const v = JSON.parse(s); return typeof v === \"object\" && v !== null && !Array.isArray(v); }\n  catch { return false; }\n}","typeGuard":"function isChainObject(raw: unknown): raw is Record<string, unknown> {\n  return typeof raw === \"object\" && raw !== null && !Array.isArray(raw);\n}","tryCatchPattern":"try { parseAudioFxChain(json); }\ncatch (err) { if (/must be a JSON object/.test(String(err))) { /* wrap nodes in {version,nodes} */ } else throw err; }","preventionTips":["Always wrap nodes in { version: HF_AUDIO_FX_CHAIN_VERSION, nodes: [...] }.","Use serializeAudioFxChain to produce the correct top-level shape."],"tags":["json","parsing","audio-fx-chain","validation","schema"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}