gastownhall/beads · error
%s must be a list of strings, got element %T
Error message
%s must be a list of strings, got element %T
What it means
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.
Source
Thrown at internal/storage/issueops/update.go:1001
return nil
}
// mergeOpStrings coerces a merge-operation value to []string. Accepts
// []interface{} of strings as well, so operation maps survive a JSON
// round-trip (e.g. daemon transports).
func mergeOpStrings(op string, value interface{}, present bool) ([]string, error) {
if !present {
return nil, nil
}
switch v := value.(type) {
case []string:
return v, nil
case []interface{}:
out := make([]string, 0, len(v))
for _, item := range v {
s, ok := item.(string)
if !ok {
return nil, fmt.Errorf("%s must be a list of strings, got element %T", op, item)
}
out = append(out, s)
}
return out, nil
default:
return nil, fmt.Errorf("%s must be a list of strings, got %T", op, value)
}
}
// readIssueAndResolveMergeOps reads the pre-update row in-transaction and folds
// any merge-operation keys (metadata edits, note appends) into concrete column
// values against that row, returning the row and the rewritten update map. It
// keeps the read-merge-write plumbing off updateIssueInTx's already-large body.
func readIssueAndResolveMergeOps(ctx context.Context, tx DBTX, id string, updates map[string]interface{}) (*types.Issue, map[string]interface{}, error) {
oldIssue, err := GetIssueInTx(ctx, tx, id)
if err != nil {
return nil, nil, fmt.Errorf("failed to get issue for update: %w", err)
}View on GitHub (pinned to 71377f2769)
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
Example fix
// before
updates["_unset_metadata"] = []interface{}{"priority", 3}
// after
updates["_unset_metadata"] = []string{"priority", "labels"} Defensive patterns
Strategy: validation
Validate before calling
func validateStringList(v interface{}) ([]string, bool) {
switch l := v.(type) {
case []string:
return l, true
case []interface{}:
out := make([]string, 0, len(l))
for _, e := range l {
s, ok := e.(string)
if !ok { return nil, false }
out = append(out, s)
}
return out, true
}
return nil, false
} Type guard
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 } } } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- %s must be a list of strings, got %T
- %w: invalid issue type %v
- metadata must be string, []byte, or json.RawMessage, got %T
- no store is open for this workspace
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/667a5c70eb66d45b.
Report an issue: GitHub.