gastownhall/beads · error · storage.ErrValidation
%w: invalid issue type %v
Error message
%w: invalid issue type %v
What it means
ValidateScalarUpdates returns this when the 'type' entry in the updates map is neither a types.IssueType nor a string — any other Go type is rejected outright before the value is checked against valid/custom types. It wraps storage.ErrValidation and includes the offending value in the message.
Source
Thrown at internal/storage/issueops/aggregate.go:119
// ValidateMetadataPatch checks mutually exclusive metadata edits.
func ValidateMetadataPatch(patch publicops.MetadataPatch) error {
if patch.Replace.Set && (patch.Merge.Set || len(patch.Set) > 0 || len(patch.Unset) > 0) {
return fmt.Errorf("%w: cannot combine metadata replacement with incremental metadata edits", storage.ErrValidation)
}
return nil
}
// ValidateScalarUpdates checks typed scalar values before they reach SQL.
func ValidateScalarUpdates(ctx context.Context, tx DBTX, updates map[string]interface{}) error {
if rawType, ok := updates["issue_type"]; ok {
var issueType types.IssueType
switch value := rawType.(type) {
case types.IssueType:
issueType = value
case string:
issueType = types.IssueType(value)
default:
return fmt.Errorf("%w: invalid issue type %v", storage.ErrValidation, rawType)
}
customTypes, err := ResolveCustomTypesInTx(ctx, tx)
if err != nil {
return fmt.Errorf("resolve custom issue types: %w", err)
}
if !issueType.IsValidWithCustom(customTypes) {
return fmt.Errorf("%w: invalid issue type %s", storage.ErrValidation, issueType)
}
}
for _, field := range []string{"assignee", "owner"} {
if raw, ok := updates[field]; ok {
if value, ok := raw.(string); ok {
if err := types.CheckFieldLen(field, value); err != nil {
return err
}
}
}
}View on GitHub (pinned to 71377f2769)
Solutions
- Convert the value to a string or types.IssueType before putting it in the updates map
- If decoding JSON, decode the type field into a string field rather than interface{}
- Check the %v value in the message to identify the unexpected Go type and fix the producer
Example fix
// before updates["type"] = 3 // int rejected // after updates["type"] = types.IssueType(3).String() // or a valid string like "bug"
Defensive patterns
Strategy: type-guard
Validate before calling
raw, ok := updates["type"]
if ok {
switch raw.(type) {
case types.IssueType, string:
default:
return fmt.Errorf("type must be string or types.IssueType, got %T", raw)
}
} Type guard
func issueTypeValue(v interface{}) (types.IssueType, bool) {
switch t := v.(type) {
case types.IssueType:
return t, true
case string:
return types.IssueType(t), true
}
return "", false
} Try / catch
if err := issueops.ValidateScalarUpdates(ctx, tx, updates); errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "invalid issue type") {
// coerce value to string/IssueType and rebuild updates map
} Prevention
- Decode JSON type fields into typed string fields, not interface{}
- Normalize with issueTypeValue() before inserting into the updates map
- Add a linter/test that walks updates maps asserting value types
When it happens
Trigger: updateIssueInTx passes updates["type"] holding an int, a *string, a custom enum, or another non-string/non-IssueType value, e.g. after JSON unmarshalling numbers into the map.
Common situations: JSON payloads with numeric type codes decoded into map[string]interface{} (numbers become float64); refactored callers passing typed wrappers or pointers; ORMs producing non-string column values.
Related errors
- %w: invalid issue type %s
- %s must be a list of strings, got element %T
- %s must be a list of strings, got %T
- metadata must be string, []byte, or json.RawMessage, got %T
- invalid issue type: %s
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/f8cd2ff5f0606341.
Report an issue: GitHub.