{"record":{"id":"8d7705e70930ad9d","repo":"gastownhall/beads","slug":"s-must-be-a-list-of-strings-got-t","errorCode":null,"errorMessage":"%s must be a list of strings, got %T","messagePattern":"(.+?) must be a list of strings, got %T","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":1007,"sourceCode":"func 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}\n\tresolved, err := ResolveMergeOps(oldIssue, updates)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn oldIssue, resolved, nil\n}","sourceCodeStart":989,"sourceCodeEnd":1025,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L989-L1025","documentation":"The default branch of mergeOpStrings fires when a merge-operation key's value is neither []string nor []interface{} — any other type is rejected. The library requires list-valued merge ops to be actual string lists so it can fold them into concrete column values against the pre-update row.","triggerScenarios":"Passing a scalar (string, int, map[string]interface{}, nil) as the value of OpMergeMetadata/OpSetMetadata/OpUnsetMetadata/OpAppendNotes in the updates map handed to ResolveMergeOps, updateIssueInTx, or storage Update.","commonSituations":"Client sends a single string instead of an array (e.g. \"labels\": \"x\" instead of [\"x\"]); a nil slip-through from an optional field; wrong op key mapping causing a scalar field value to be treated as a merge op.","solutions":["Wrap scalar values in a single-element []string before adding them to the update map","Check the op key spelling — a merge-op key set by mistake on a scalar field will be type-checked as a list","If value comes from decoded JSON, assert the decoded type is an array of strings before calling Update","Use a nil-check: nil values should either be omitted or sent via the regular column key, not a merge-op key"],"exampleFix":"// before\nupdates[\"_append_notes\"] = \"my note\"\n// after\nupdates[\"_append_notes\"] = []string{\"my note\"}","handlingStrategy":"validation","validationCode":"func isStringSliceOrWrap(v interface{}) ([]string, bool) {\n    switch l := v.(type) {\n    case []string:\n        return l, true\n    case string:\n        return []string{l}, true\n    case nil:\n        return nil, false\n    }\n    return nil, false\n}","typeGuard":"if _, ok := updates[OpAppendNotes].([]string); !ok { return fmt.Errorf(\"%s requires a []string\", OpAppendNotes) }","tryCatchPattern":null,"preventionTips":["Always pass merge-op values as []string (or []interface{} of strings)","Check op-key spelling so scalar fields are not misread as list merge ops","Add a small helper that normalizes scalars into single-element slices before Update"],"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"}