{"record":{"id":"53768359a04e7b40","repo":"gastownhall/beads","slug":"truncated-json-escape","errorCode":null,"errorMessage":"truncated JSON escape","messagePattern":"truncated JSON escape","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":644,"sourceCode":"\treturn checkUTF8(fields...)\n}\n\nfunc checkUTF8(fields ...currentString) error {\n\tfor _, field := range fields {\n\t\tif !utf8.ValidString(field.value) {\n\t\t\treturn fmt.Errorf(\"legacy SQLite %s contains invalid UTF-8\", field.name)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkJSONSurrogates(raw string) error {\n\tfor i := 0; i < len(raw); i++ {\n\t\tif raw[i] != '\\\\' {\n\t\t\tcontinue\n\t\t}\n\t\tif i+1 >= len(raw) {\n\t\t\treturn fmt.Errorf(\"truncated JSON escape\")\n\t\t}\n\t\tif raw[i+1] != 'u' {\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif i+6 > len(raw) {\n\t\t\treturn fmt.Errorf(\"truncated JSON Unicode escape\")\n\t\t}\n\t\tcode, err := strconv.ParseUint(raw[i+2:i+6], 16, 16)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid JSON Unicode escape\")\n\t\t}\n\t\tswitch {\n\t\tcase code >= 0xd800 && code <= 0xdbff:\n\t\t\tnext := i + 6\n\t\t\tif next+6 > len(raw) || raw[next] != '\\\\' || raw[next+1] != 'u' {\n\t\t\t\treturn fmt.Errorf(\"lone high UTF-16 surrogate escape\")\n\t\t\t}","sourceCodeStart":626,"sourceCodeEnd":662,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L626-L662","documentation":"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.","triggerScenarios":"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).","commonSituations":"Hand-edited or corrupted legacy rows, data written by tools that escaped improperly, or truncated TEXT values from an old export.","solutions":["Inspect the offending legacy row and remove or properly escape the trailing backslash (use '\\\\\\\\' in the source data).","Re-export the legacy database with a tool that produces valid JSON escaping.","Sanitize string columns before migration, e.g. double any trailing backslash."],"exampleFix":"// before (legacy row value)\n\"note\": \"saved with trailing slash \\\\\n// after\n\"note\": \"saved with trailing slash \\\\\\\\\"","handlingStrategy":"validation","validationCode":"if strings.HasSuffix(raw, \"\\\\\") { return fmt.Errorf(\"value ends with a lone backslash: %q\", raw) }","typeGuard":"func hasTrailingBackslash(s string) bool { return strings.HasSuffix(s, \"\\\\\") }","tryCatchPattern":null,"preventionTips":["Always produce JSON with a standard encoder, never manual string concatenation","Validate legacy TEXT columns with a JSON-escape linter before migration","Reject trailing-backslash strings at export time"],"tags":["json","escaping","migration","sqlite"],"backgroundTag":"invalid-json-escape","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}