{"record":{"id":"5c4097678b1fee46","repo":"gastownhall/beads","slug":"must-be-an-array-of-strings","errorCode":null,"errorMessage":"must be an array of strings","messagePattern":"must be an array of strings","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":695,"sourceCode":"func validateCurrentTextBytes(issue *types.Issue) error {\n\tif len(issue.Payload) > currentTextBytes {\n\t\treturn fmt.Errorf(\"legacy SQLite issue payload is %d bytes (current TEXT maximum %d)\", len(issue.Payload), currentTextBytes)\n\t}\n\twaiters := issueops.FormatJSONStringArray(issue.Waiters)\n\tif len(waiters) > currentTextBytes {\n\t\treturn fmt.Errorf(\"legacy SQLite issue waiters serialize to %d bytes (current TEXT maximum %d)\", len(waiters), currentTextBytes)\n\t}\n\treturn nil\n}\n\nfunc decodeWaiters(raw string) ([]string, error) {\n\tvar decoded any\n\tif err := json.Unmarshal([]byte(raw), &decoded); err != nil {\n\t\treturn nil, err\n\t}\n\tvalues, ok := decoded.([]any)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"must be an array of strings\")\n\t}\n\twaiters := make([]string, len(values))\n\tfor i, value := range values {\n\t\twaiter, ok := value.(string)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"element %d is not a string\", i)\n\t\t}\n\t\tif err := checkUTF8(currentString{fmt.Sprintf(\"waiters[%d]\", i), waiter}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\twaiters[i] = waiter\n\t}\n\treturn waiters, nil\n}\n\nfunc validateIssueVarchars(issue *types.Issue) error {\n\tfields := []currentVarchar{\n\t\t{\"issue id\", issue.ID, types.MaxFieldLen},","sourceCodeStart":677,"sourceCodeEnd":713,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L677-L713","documentation":"decodeWaiters parses the legacy waiters column as JSON and requires the top-level value to be a JSON array. If json.Unmarshal succeeds but the value is not an array (object, string, number, etc.), the migration returns 'must be an array of strings'.","triggerScenarios":"decodeWaiters receives a raw string that unmarshals to a non-[]any value, e.g. '{\"a\":1}' or '\"foo\"'.","commonSituations":"Waiters stored in an older format (CSV, object map) in the legacy DB, hand-edited rows, or a schema change between legacy versions.","solutions":["Convert the legacy waiters value to a JSON array of strings before migration.","Identify the legacy format and write a converter to the expected array form.","Correct the malformed row directly in the legacy database."],"exampleFix":"// before (legacy value)\n{\"waiters\": [\"alice\"]}\n// after\n[\"alice\"]","handlingStrategy":"type-guard","validationCode":"var v any; if json.Unmarshal([]byte(raw), &v) != nil || _, ok := v.([]any); !ok { return errors.New(\"waiters must be a JSON array\") }","typeGuard":"func isStringArray(v any) bool { _, ok := v.([]any); return ok }","tryCatchPattern":"waiters, err := decodeWaiters(raw); if err != nil { log.Warn(\"unparseable waiters, defaulting to empty\", err); waiters = nil }","preventionTips":["Store waiters only as JSON arrays of strings","Version the legacy schema and write converters per version","Never hand-edit JSON columns in the DB"],"tags":["json","waiters","migration","type-mismatch"],"backgroundTag":"json-type-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}