JuliusBrussee/caveman · error

object key is %T

Error message

object key is %T

What it means

During JSON-to-TOON parsing, readJSONTOONValue expects object keys to be JSON strings. The encoding/json decoder normally guarantees this, so "object key is %T" fires only if a non-string token (e.g. json.Delim or number) appears where a key is expected — meaning the token stream is malformed or the parser's state machine drifted. The error includes the Go type of the offending token for debugging.

Solutions

  1. Use the standard encoding/json Decoder directly — it only emits strings as object keys; remove custom decoder wrappers.
  2. Check call sites of readJSONTOONValue for off-by-one token consumption (reading a value position as a key).
  3. Add a regression test with nested objects/arrays to lock the token-stream state machine.

Example fix

// before: state drift
val, _ := readJSONTOONValue(dec) // consumed '}' already
keyTok, _ := dec.Token() // now Delim
// after: check delim before parsing fields
tok, _ := dec.Token()
d, ok := tok.(json.Delim); if !ok || d != '{' { return err }
Defensive patterns

Strategy: validation

Validate before calling

var probe map[string]any
if err := json.Unmarshal(input, &probe); err != nil { /* not valid JSON — reject before TOON parse */ }

Type guard

func tokIsString(tok json.Token) bool { _, ok := tok.(string); return ok }

Try / catch

v, err := parseJSONTOON(input)
if err != nil && strings.HasPrefix(err.Error(), "object key is") {
    return fmt.Errorf("malformed token stream: %w", err)
}

Prevention

When it happens

Trigger: In readJSONTOONValue's object branch, dec.Token() yields a token whose type assertion keyTok.(string) fails while reading an object's key position — reached from parseJSONTOON or nested recursion.

Common situations: A bug in a custom json.Token stream or a wrapper decoder around the standard one; parser refactors that call readJSONTOONValue after already consuming the closing delimiter; corrupted/injected token streams in tests.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/822c25d650b9e444. Report an issue: GitHub.

Appendix: source

Thrown at engine/compressors/toon_encode.go:141

func readJSONTOONValue(dec *json.Decoder) (toonValue, error) {
	tok, err := dec.Token()
	if err != nil {
		return toonValue{}, err
	}
	switch t := tok.(type) {
	case json.Delim:
		switch t {
		case '{':
			var fields []toonField
			seen := make(map[string]struct{})
			for dec.More() {
				keyTok, err := dec.Token()
				if err != nil {
					return toonValue{}, err
				}
				key, ok := keyTok.(string)
				if !ok {
					return toonValue{}, fmt.Errorf("object key is %T", keyTok)
				}
				if _, duplicate := seen[key]; duplicate {
					return toonValue{}, fmt.Errorf("duplicate object key %q", key)
				}
				seen[key] = struct{}{}
				val, err := readJSONTOONValue(dec)
				if err != nil {
					return toonValue{}, err
				}
				fields = append(fields, toonField{name: key, value: val})
			}
			end, err := dec.Token()
			if err != nil {
				return toonValue{}, err
			}
			if d, ok := end.(json.Delim); !ok || d != '}' {
				return toonValue{}, fmt.Errorf("object not closed")
			}

View on GitHub (pinned to 3ee70a1026)