gastownhall/beads · error

truncated JSON escape

Error message

truncated JSON escape

What it means

During legacy SQLite migration, checkJSONSurrogates scans issue metadata strings for backslash escapes before they are re-encoded. If the raw string ends with a lone backslash, there is no following escape character, so the string is not valid JSON and the migration aborts with 'truncated JSON escape'. This guards against importing malformed escaped text from a legacy database.

Source

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

	return checkUTF8(fields...)
}

func checkUTF8(fields ...currentString) error {
	for _, field := range fields {
		if !utf8.ValidString(field.value) {
			return fmt.Errorf("legacy SQLite %s contains invalid UTF-8", field.name)
		}
	}
	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")
			}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the offending legacy row and remove or properly escape the trailing backslash (use '\\\\' in the source data).
  2. Re-export the legacy database with a tool that produces valid JSON escaping.
  3. Sanitize string columns before migration, e.g. double any trailing backslash.

Example fix

// before (legacy row value)
"note": "saved with trailing slash \\
// after
"note": "saved with trailing slash \\\\"
Defensive patterns

Strategy: validation

Validate before calling

if strings.HasSuffix(raw, "\\") { return fmt.Errorf("value ends with a lone backslash: %q", raw) }

Type guard

func hasTrailingBackslash(s string) bool { return strings.HasSuffix(s, "\\") }

Prevention

When it happens

Trigger: A string column read from the legacy SQLite DB ends with a bare '\' character as the final byte, so raw[i]=='\\' with i+1 >= len(raw).

Common situations: Hand-edited or corrupted legacy rows, data written by tools that escaped improperly, or truncated TEXT values from an old export.

Related errors


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