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
- 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
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
- 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
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
- Top-level document must start with a key-value or array-head
- Indentation depth jump: expected depth ${parentDepth + 1}, b
- Unexpected bare token line outside root primitive position
- Unexpected content after the document root
- Unexpected indentation inside keyed tabular object
AI-assisted analysis of toon-format/toon@604eac266e (2026-08-31).
Data as JSON: /api/errors/c7a5c594c7ae6d19.
Report an issue: GitHub.