toon-format/toon · error · Error

Primitive event without preceding key in object

Error message

Primitive event without preceding key in object

What it means

The event-builder received a `primitive` event while the current container is an object whose `currentKey` is undefined. In TOON's stream-event contract every value inside an object must be preceded by a `key` event, so a primitive with no pending key is a malformed event stream and the builder aborts instead of silently dropping the value. This guards against producers that emit events out of order.

Solutions

  1. Fix the event producer so every primitive inside an object is preceded by a `key` event
  2. If you control the stream, log the events immediately before the failure to find the missing key
  3. Use the official toon encode/decode APIs instead of hand-assembling events
  4. If filtering/transforming event streams, ensure transforms preserve key/value pairing

Example fix

// before: events = [{type:'startObject'},{type:'primitive',value:42},{type:'endObject'}]
// after:  events = [{type:'startObject'},{type:'key',key:'a'},{type:'primitive',value:42},{type:'endObject'}]
Defensive patterns

Strategy: validation

Validate before calling

function isValidEventStream(events) {
  let inObject = false, pendingKey = false
  for (const e of events) {
    if (e.type === 'startObject') { inObject = true; pendingKey = false }
    else if (e.type === 'endObject') { inObject = false; pendingKey = false }
    else if (e.type === 'key') { if (!inObject) return false; pendingKey = true }
    else if (e.type === 'primitive') { if (inObject && !pendingKey) return false; pendingKey = false }
  }
  return true
}

Try / catch

try {
  const value = buildValueFromEvents(events)
} catch (err) {
  if (err instanceof Error && err.message.includes('without preceding key')) {
    // regenerate events or reject malformed stream
  } else throw err
}

Prevention

When it happens

Trigger: Calling buildValueFromEvents (or the async variant) with an event iterable where a `primitive` event appears inside an object without a preceding `key` event — e.g. hand-written event generators, custom tokenizers, or a buggy custom encoder that skips key events.

Common situations: Developers writing custom TOON encoders or streaming pipelines that synthesize JsonStreamEvent sequences themselves; tools post-processing or filtering event streams and accidentally removing key events; version mismatches where an older producer emits a different event grammar than the decoder expects.

Related errors


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

Appendix: source

Thrown at packages/toon/src/decode/event-builder.ts:159

      const parent = stack[stack.length - 1]!
      if (parent.type !== 'object') {
        throw new Error('Key event outside of object context')
      }

      parent.currentKey = event.key

      break
    }

    case 'primitive': {
      if (stack.length === 0) {
        state.root = event.value
      }
      else {
        const parent = stack[stack.length - 1]!
        if (parent.type === 'object') {
          if (parent.currentKey === undefined) {
            throw new Error('Primitive event without preceding key in object')
          }
          setOwnProperty(parent.obj, parent.currentKey, event.value)
          parent.currentKey = undefined
        }
        else if (parent.type === 'array') {
          parent.arr.push(event.value)
        }
      }

      break
    }
  }
}

function finalizeState(state: BuildState): JsonValue {
  if (state.stack.length !== 0) {
    throw new Error('Incomplete event stream: unclosed objects or arrays')
  }

View on GitHub (pinned to 604eac266e)