{"record":{"id":"e3123a69584a70cc","repo":"toon-format/toon","slug":"expected-header-length-tabular-rows-but-found","errorCode":null,"errorMessage":"Expected ${header.length} tabular rows, but found more","messagePattern":"Expected (.+?) tabular rows, but found more","errorType":"exception","errorClass":"ToonDecodeError","httpStatus":null,"severity":"error","filePath":"packages/toon/src/decode/validation.ts","lineNumber":46,"sourceCode":"  if (nextLine?.depth === itemDepth && nextLine.content.startsWith(LIST_ITEM_PREFIX)) {\n    throw new ToonDecodeError(\n      `Expected ${expectedCount} list-form items, but found more`,\n      { line: nextLine.lineNumber, source: nextLine.raw },\n    )\n  }\n}\n\nexport function validateNoExtraTabularRows(\n  nextLine: ParsedLine | undefined,\n  rowDepth: Depth,\n  header: ArrayHeaderInfo,\n): void {\n  if (\n    nextLine?.depth === rowDepth\n    && !nextLine.content.startsWith(LIST_ITEM_PREFIX)\n    && isDataRow(nextLine.content, header.delimiter)\n  ) {\n    throw new ToonDecodeError(\n      `Expected ${header.length} tabular rows, but found more`,\n      { line: nextLine.lineNumber, source: nextLine.raw },\n    )\n  }\n}\n\nexport function validateNoBlankLinesInRange(\n  startLine: number,\n  endLine: number,\n  blankLines: BlankLineInfo[],\n  strict: boolean,\n  context: string,\n): void {\n  if (!strict)\n    return\n\n  const firstBlank = blankLines.find(\n    blank => blank.lineNumber > startLine && blank.lineNumber < endLine,","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/toon-format/toon/blob/604eac266e35166bed6f5b9e3bd586e9c60a9330/packages/toon/src/decode/validation.ts#L28-L64","documentation":"Thrown by validateNoExtraTabularRows when a tabular array (header like `items[N]{a,b}`) contains more data rows at row depth than the declared count N: the following line is at row depth, is not a list bullet, and is classified as a data row (isDataRow). This stops the decoder from silently consuming rows that belong to another structure or were added without updating the header.","triggerScenarios":"Decoding a tabular array (decodeTabularArray) where the number of pipe/delimiter-separated rows at row depth exceeds header.length.","commonSituations":"Spreadsheet exports with rows appended after the count was computed; merging two row blocks; hand-editing a row but not the `[N]` header; a line that looks like a data row (delimiter before any unquoted colon) accidentally placed inside the array block.","solutions":["Remove the extra rows so the body matches header.length","Update the [N] header count to the actual row count","Check the extra line — it may be a following key-value block misindented to row depth; fix its indentation instead"],"exampleFix":"// before\nusers[2]{id,name}\n  1,Alice\n  2,Bob\n  3,Carol\n// after\nusers[3]{id,name}\n  1,Alice\n  2,Bob\n  3,Carol","handlingStrategy":"validation","validationCode":"const rows = block.slice(1).filter(l => !l.startsWith('- '))\nif (rows.length !== headerCount) throw new Error(`tabular block has ${rows.length} rows, header declares ${headerCount}`)","typeGuard":"function tabularRowCountsMatch(headerCount: number, rows: string[]): boolean { return rows.length === headerCount }","tryCatchPattern":"try { decode(input) } catch (e) { if (e instanceof ToonDecodeError && e.message.includes('tabular rows, but found more')) console.error('extra row near line', e.line?.lineNumber, e.line?.source) }","preventionTips":["Keep tabular row generation and header count in the same code path","Check indentation of lines after the array — the 'extra row' may be a misindented key-value pair","Lint TOON payloads before decoding external input"],"tags":["toon","decode","validation","tabular"],"backgroundTag":"count-mismatch","analyzedSha":"604eac266e35166bed6f5b9e3bd586e9c60a9330","analyzedAt":"2026-08-31T11:09:25.043Z","schemaVersion":2},"datasetVersion":"2026-09-01T08:17:40.651Z"}