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

  1. Convert the value to a string or types.IssueType before putting it in the updates map
  2. If decoding JSON, decode the type field into a string field rather than interface{}
  3. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/f8cd2ff5f0606341. Report an issue: GitHub.