{"record":{"id":"667a5c70eb66d45b","repo":"gastownhall/beads","slug":"s-must-be-a-list-of-strings-got-element-t","errorCode":null,"errorMessage":"%s must be a list of strings, got element %T","messagePattern":"(.+?) must be a list of strings, got element %T","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":1001,"sourceCode":"\treturn nil\n}\n\n// mergeOpStrings coerces a merge-operation value to []string. Accepts\n// []interface{} of strings as well, so operation maps survive a JSON\n// round-trip (e.g. daemon transports).\nfunc mergeOpStrings(op string, value interface{}, present bool) ([]string, error) {\n\tif !present {\n\t\treturn nil, nil\n\t}\n\tswitch v := value.(type) {\n\tcase []string:\n\t\treturn v, nil\n\tcase []interface{}:\n\t\tout := make([]string, 0, len(v))\n\t\tfor _, item := range v {\n\t\t\ts, ok := item.(string)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"%s must be a list of strings, got element %T\", op, item)\n\t\t\t}\n\t\t\tout = append(out, s)\n\t\t}\n\t\treturn out, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"%s must be a list of strings, got %T\", op, value)\n\t}\n}\n\n// readIssueAndResolveMergeOps reads the pre-update row in-transaction and folds\n// any merge-operation keys (metadata edits, note appends) into concrete column\n// values against that row, returning the row and the rewritten update map. It\n// keeps the read-merge-write plumbing off updateIssueInTx's already-large body.\nfunc readIssueAndResolveMergeOps(ctx context.Context, tx DBTX, id string, updates map[string]interface{}) (*types.Issue, map[string]interface{}, error) {\n\toldIssue, err := GetIssueInTx(ctx, tx, id)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to get issue for update: %w\", err)\n\t}","sourceCodeStart":983,"sourceCodeEnd":1019,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L983-L1019","documentation":"mergeOpStrings validates that a list-valued merge-operation key (e.g. metadata unset/set list or appended notes) passed to the issue update map contains only strings. When a []interface{} slice carries a non-string element, resolution of the merge op is aborted so a malformed value never reaches the SQL column. This protects the read-merge-write path from type-unsafe payloads that typically arrive over JSON or daemon transports.","triggerScenarios":"Passing updates[OpSetMetadata], updates[OpUnsetMetadata], or updates[OpAppendNotes] as []interface{} containing at least one non-string element (e.g. []interface{}{\"a\", 42}) to ResolveMergeOps/updateIssueInTx, typically after decoding user JSON without enforcing a string array.","commonSituations":"JSON payloads where a client sent [\"key1\", 3] or [null] for metadata unset; YAML/JSON decoding into interface{}; dynamic CLI flag values interpolated into a list; template-generated update maps.","solutions":["Inspect the update map value at the merge-op key and ensure every element is a string before calling Update/ResolveMergeOps","If the value comes from JSON, decode into []string instead of []interface{}, or validate elements client-side","Convert numeric/other elements explicitly with strconv / fmt.Sprintf if they are meant to be string keys","Remove null/nil or placeholder entries from the list"],"exampleFix":"// before\nupdates[\"_unset_metadata\"] = []interface{}{\"priority\", 3}\n// after\nupdates[\"_unset_metadata\"] = []string{\"priority\", \"labels\"}","handlingStrategy":"validation","validationCode":"func validateStringList(v interface{}) ([]string, bool) {\n    switch l := v.(type) {\n    case []string:\n        return l, true\n    case []interface{}:\n        out := make([]string, 0, len(l))\n        for _, e := range l {\n            s, ok := e.(string)\n            if !ok { return nil, false }\n            out = append(out, s)\n        }\n        return out, true\n    }\n    return nil, false\n}","typeGuard":"if list, ok := value.([]string); ok { /* safe */ } else if raw, ok := value.([]interface{}); ok { for _, e := range raw { if _, ok := e.(string); !ok { return error } } }","tryCatchPattern":null,"preventionTips":["Decode JSON merge-op payloads into []string, not []interface{}","Validate user-supplied lists element-by-element before building the update map","Never interpolate raw config values into merge-op keys without a string assertion"],"tags":["go","validation","merge-ops","type-mismatch"],"backgroundTag":"invalid-list-element-type","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}