{"record":{"id":"c7a5c594c7ae6d19","repo":"toon-format/toon","slug":"duplicate-sibling-key-key","errorCode":null,"errorMessage":"Duplicate sibling key \"${key}\"","messagePattern":"Duplicate sibling key \"(.+?)\"","errorType":"exception","errorClass":"ToonDecodeError","httpStatus":null,"severity":"error","filePath":"packages/toon/src/decode/decoders.ts","lineNumber":184,"sourceCode":"// Strict decoding never silently discards input, so a line after the root form is an error.\nfunction* assertFullyConsumed(reader: LineReader, strict: boolean): LineRule {\n  if (!strict) {\n    return\n  }\n  const line = yield* peekLine(reader)\n  if (line) {\n    throw new ToonDecodeError(\n      'Unexpected content after the document root',\n      { line: line.lineNumber, source: line.raw },\n    )\n  }\n}\n\nfunction assertNoDuplicateKey(key: string, line: ParsedLine, seenKeys: Set<string> | undefined): void {\n  if (!seenKeys)\n    return\n  if (seenKeys.has(key)) {\n    throw new ToonDecodeError(\n      `Duplicate sibling key \"${key}\"`,\n      { line: line.lineNumber, source: line.raw },\n    )\n  }\n  seenKeys.add(key)\n}\n\n// #endregion\n\n// #region Decode rules\n\nfunction* decodeKeyValue(\n  line: ParsedLine,\n  reader: LineReader,\n  baseDepth: Depth,\n  options: DecoderContext,\n  seenKeys?: Set<string>,\n): LineRule {","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/toon-format/toon/blob/604eac266e35166bed6f5b9e3bd586e9c60a9330/packages/toon/src/decode/decoders.ts#L166-L202","documentation":"Strict mode forbids the same key appearing twice among siblings in an object; later values would silently overwrite earlier ones otherwise. assertNoDuplicateKey tracks every key in the strict-mode seenKeys set and throws on the second occurrence of a key at the same level.","triggerScenarios":"decodeKeyValue or decodeKeyedObject adding a key already present in seenKeys (created when options.strict is true), e.g. `name: a` appearing twice inside the same object block.","commonSituations":"Merging config snippets by concatenation; manual edits that re-add an existing key; generated output from a buggy serializer; copy-pasted blocks inside the same object.","solutions":["Remove or rename the duplicate key","Merge the duplicate values into a single entry or an array `[2]: ...`","Decode with strict:false to allow last-write-wins semantics","Fix the generator/serializer producing duplicate keys"],"exampleFix":"// before\nuser:\n  name: Ada\n  name: Lovelace\n\n// after\nuser:\n  name: Ada Lovelace","handlingStrategy":"validation","validationCode":"function hasNoDuplicateKeys(input: string): boolean {\n  const stack: string[][] = []\n  for (const raw of input.split('\\n')) {\n    if (!raw.trim()) continue\n    const depth = Math.floor(((raw.match(/^ */) ?? [''])[0].length) / 2)\n    stack.length = depth\n    const m = raw.trim().match(/^([^:]+):/)\n    if (m && !raw.trim().startsWith('- ')) {\n      const sib = stack[depth] ?? (stack[depth] = [])\n      if (sib.includes(m[1])) return false\n      sib.push(m[1])\n    }\n  }\n  return true\n}","typeGuard":null,"tryCatchPattern":"try {\n  return decode(input, { strict: true })\n} catch (e) {\n  if (e instanceof ToonDecodeError && e.message.includes('Duplicate sibling key')) {\n    return decode(input, { strict: false }) // last-write-wins\n  }\n  throw e\n}","preventionTips":["Deduplicate keys at the source before serializing","When merging configs, merge objects key-wise instead of concatenating text","Review diff-merge results for re-added keys"],"tags":["toon","decoder","strict-mode","duplicate-key"],"backgroundTag":"duplicate-key","analyzedSha":"604eac266e35166bed6f5b9e3bd586e9c60a9330","analyzedAt":"2026-08-31T11:09:25.043Z","schemaVersion":2},"datasetVersion":"2026-09-01T08:17:40.651Z"}