gastownhall/beads · error

truncated JSON Unicode escape

Error message

truncated JSON Unicode escape

What it means

checkJSONSurrogates validates \uXXXX escapes in metadata strings. If a backslash is followed by 'u' but fewer than four hex digits remain in the string, the Unicode escape is incomplete and cannot be decoded, so the migration returns 'truncated JSON Unicode escape'.

Source

Thrown at internal/migration/legacysqlite/reader.go:651

		}
	}
	return nil
}

func checkJSONSurrogates(raw string) error {
	for i := 0; i < len(raw); i++ {
		if raw[i] != '\\' {
			continue
		}
		if i+1 >= len(raw) {
			return fmt.Errorf("truncated JSON escape")
		}
		if raw[i+1] != 'u' {
			i++
			continue
		}
		if i+6 > len(raw) {
			return fmt.Errorf("truncated JSON Unicode escape")
		}
		code, err := strconv.ParseUint(raw[i+2:i+6], 16, 16)
		if err != nil {
			return fmt.Errorf("invalid JSON Unicode escape")
		}
		switch {
		case code >= 0xd800 && code <= 0xdbff:
			next := i + 6
			if next+6 > len(raw) || raw[next] != '\\' || raw[next+1] != 'u' {
				return fmt.Errorf("lone high UTF-16 surrogate escape")
			}
			low, err := strconv.ParseUint(raw[next+2:next+6], 16, 16)
			if err != nil || low < 0xdc00 || low > 0xdfff {
				return fmt.Errorf("lone high UTF-16 surrogate escape")
			}
			i = next + 5
		case code >= 0xdc00 && code <= 0xdfff:
			return fmt.Errorf("lone low UTF-16 surrogate escape")

View on GitHub (pinned to 71377f2769)

Solutions

  1. Locate and repair the truncated \uXXXX escape in the legacy row (complete the four hex digits or remove the partial escape).
  2. Re-export the legacy database without byte truncation.
  3. Sanitize inputs to expand or strip partial Unicode escapes before migration.

Example fix

// before
"label": "caf\u00
// after
"label": "caf\u00e9"
Defensive patterns

Strategy: validation

Validate before calling

for i := 0; i+1 < len(raw); i++ { if raw[i]=='\\' && raw[i+1]=='u' && i+6 > len(raw) { return fmt.Errorf("truncated \\u escape at %d", i) } }

Type guard

func completeUnicodeEscapes(s string) bool { for i := 0; i < len(s); i++ { if s[i]=='\\' && i+1 < len(s) && s[i+1]=='u' && i+6 > len(s) { return false } }; return true }

Prevention

When it happens

Trigger: A raw string contains '\u' (or '\u1') within the last 5 bytes so that i+6 exceeds len(raw).

Common situations: Legacy rows truncated mid-escape by a byte-limit, corrupted exports, or manual edits that cut a string in the middle of a \uXXXX sequence.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/b5927356bac9dfec. Report an issue: GitHub.