toon-format/toon · error · ToonDecodeError

Duplicate sibling key "${key}"

Error message

Duplicate sibling key "${key}"

What it means

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.

Source

Thrown at packages/toon/src/decode/decoders.ts:184

// Strict decoding never silently discards input, so a line after the root form is an error.
function* assertFullyConsumed(reader: LineReader, strict: boolean): LineRule {
  if (!strict) {
    return
  }
  const line = yield* peekLine(reader)
  if (line) {
    throw new ToonDecodeError(
      'Unexpected content after the document root',
      { line: line.lineNumber, source: line.raw },
    )
  }
}

function assertNoDuplicateKey(key: string, line: ParsedLine, seenKeys: Set<string> | undefined): void {
  if (!seenKeys)
    return
  if (seenKeys.has(key)) {
    throw new ToonDecodeError(
      `Duplicate sibling key "${key}"`,
      { line: line.lineNumber, source: line.raw },
    )
  }
  seenKeys.add(key)
}

// #endregion

// #region Decode rules

function* decodeKeyValue(
  line: ParsedLine,
  reader: LineReader,
  baseDepth: Depth,
  options: DecoderContext,
  seenKeys?: Set<string>,
): LineRule {

View on GitHub (pinned to 604eac266e)

Solutions

  1. Remove or rename the duplicate key
  2. Merge the duplicate values into a single entry or an array `[2]: ...`
  3. Decode with strict:false to allow last-write-wins semantics
  4. Fix the generator/serializer producing duplicate keys

Example fix

// before
user:
  name: Ada
  name: Lovelace

// after
user:
  name: Ada Lovelace
Defensive patterns

Strategy: validation

Validate before calling

function hasNoDuplicateKeys(input: string): boolean {
  const stack: string[][] = []
  for (const raw of input.split('\n')) {
    if (!raw.trim()) continue
    const depth = Math.floor(((raw.match(/^ */) ?? [''])[0].length) / 2)
    stack.length = depth
    const m = raw.trim().match(/^([^:]+):/)
    if (m && !raw.trim().startsWith('- ')) {
      const sib = stack[depth] ?? (stack[depth] = [])
      if (sib.includes(m[1])) return false
      sib.push(m[1])
    }
  }
  return true
}

Try / catch

try {
  return decode(input, { strict: true })
} catch (e) {
  if (e instanceof ToonDecodeError && e.message.includes('Duplicate sibling key')) {
    return decode(input, { strict: false }) // last-write-wins
  }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of toon-format/toon@604eac266e (2026-08-31). Data as JSON: /api/errors/c7a5c594c7ae6d19. Report an issue: GitHub.