toon-format/toon · error · ToonDecodeError

Top-level document must start with a key-value or array-head

Error message

Top-level document must start with a key-value or array-header line

What it means

TOON documents must begin with either a single primitive value or a top-level `key: value` / `[N]:` array-header line. decodeDocument throws this when the first line is neither a parseable primitive nor a key-value line while the stream is not a one-line primitive document, so no valid root can be established. It is a structural error in the input text, not a runtime state problem.

Source

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

    const headerInfo = withLine(first, () => resolveArrayHeader(parseArrayHeaderLine(first.content, DEFAULT_DELIMITER), options.strict))
    if (headerInfo) {
      yield* readLine(reader)
      yield* decodeArrayFromHeader(headerInfo.header, headerInfo.inlineValues, reader, 0, options, first)
      yield* assertFullyConsumed(reader, options.strict)
      return
    }
  }

  yield* readLine(reader)
  const following = yield* peekLine(reader)
  const hasMore = following !== undefined
  if (!hasMore && !isKeyValueLine(first)) {
    yield { type: 'primitive', value: withLine(first, () => parsePrimitiveToken(first.content)) }
    return
  }

  if (!isKeyValueLine(first) && following?.depth === 0) {
    throw new ToonDecodeError(
      'Top-level document must start with a key-value or array-header line',
      { line: first.lineNumber, source: first.raw },
    )
  }

  const rootSeenKeys = options.strict ? new Set<string>() : undefined
  yield { type: 'startObject' }
  yield* decodeKeyValue(first, reader, 0, options, rootSeenKeys)

  while (true) {
    const line = yield* peekLine(reader)
    if (!line) {
      break
    }

    if (line.depth !== 0) {
      if (options.strict) {
        throw overIndentedLineError(line, 0)

View on GitHub (pinned to 604eac266e)

Solutions

  1. Start the document with a `key: value` line or an `[N]{...}:` array header at depth 0
  2. If the input is a single bare value, ensure it is the only line so the primitive-root path is taken
  3. Check for missing/renamed root key or accidental leading blank/garbage lines
  4. Wrap the fragment in a root key before decoding

Example fix

// before
"just a bare value"
another line

// after
root: "just a bare value"
nested:
  another: line
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeToonDocument(text: string): boolean {
  const first = text.split('\n', 1)[0]?.trim() ?? ''
  if (!first) return false
  if (first.startsWith('- ')) return false
  return /^[^:\s][^:]*:/.test(first) || /^\[\d+\]/.test(first)
}
if (!looksLikeToonDocument(input)) throw new Error('Input is not a rooted TOON document')

Type guard

function isRootedToon(text: string): boolean {
  const first = text.split('\n', 1)[0] ?? ''
  return /:\s*/.test(first) || /^\[\d+\]/.test(first)
}

Try / catch

try {
  const doc = decode(input, { strict: true })
} catch (e) {
  if (e instanceof ToonDecodeError && /Top-level document/.test(e.message)) {
    // recover: wrap fragment or surface a user-facing parse error
  } else throw e
}

Prevention

When it happens

Trigger: Calling decodeStreamSync/decodeStream (or decode) on input whose first line is a bare token that contains no unquoted colon and is not a `- ` list item, and where a following line exists at depth 0 (so it cannot be treated as a lone root primitive).

Common situations: Hand-written TOON files that start with a stray value or comment-like text; truncated files where the opening key was lost; pasting only a nested fragment (e.g. the body of an object) instead of the whole document; tooling that emits YAML with the root key stripped.

Related errors


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