gastownhall/beads · error

metadata must be string, []byte, or json.RawMessage, got %T

Error message

metadata must be string, []byte, or json.RawMessage, got %T

What it means

NormalizeMetadataValue was given a metadata value whose Go type is not one of the accepted types (string, []byte, json.RawMessage). The storage layer stores metadata as a JSON string, so callers must pass one of those shapes; e.g. passing a map[string]interface{} or a struct directly is rejected with the actual %T type named in the message.

Source

Thrown at internal/storage/metadata.go:27

)

// NormalizeMetadataValue converts metadata values to a validated JSON string.
// Accepts string, []byte, or json.RawMessage and returns a validated JSON string.
// Returns an error if the value is not valid JSON or is an unsupported type.
//
// This supports GH#1417: allow UpdateIssue metadata updates via json.RawMessage/[]byte.
func NormalizeMetadataValue(value interface{}) (string, error) {
	var jsonStr string

	switch v := value.(type) {
	case string:
		jsonStr = v
	case []byte:
		jsonStr = string(v)
	case json.RawMessage:
		jsonStr = string(v)
	default:
		return "", fmt.Errorf("metadata must be string, []byte, or json.RawMessage, got %T", value)
	}

	// Validate that it's valid JSON
	if !json.Valid([]byte(jsonStr)) {
		return "", fmt.Errorf("metadata is not valid JSON")
	}

	return jsonStr, nil
}

// MetadataFieldType defines the type of a metadata field for schema validation.
type MetadataFieldType string

const (
	MetadataFieldString MetadataFieldType = "string"
	MetadataFieldInt    MetadataFieldType = "int"
	MetadataFieldFloat  MetadataFieldType = "float"
	MetadataFieldBool   MetadataFieldType = "bool"

View on GitHub (pinned to 71377f2769)

Solutions

  1. Marshal the value first: b, _ := json.Marshal(v), then pass json.RawMessage(b) or string(b)
  2. If passing a string, ensure it is JSON text (an object literal like `{"k":"v"}`), not raw key=value text
  3. For clearing metadata pass the string "{}" or "null" (both are valid JSON), not nil
  4. Check the %T in the error message to see which type was actually passed

Example fix

// before
updates := map[string]interface{}{"metadata": map[string]interface{}{"key": "value"}}
// after
b, _ := json.Marshal(map[string]interface{}{"key": "value"})
updates := map[string]interface{}{"metadata": json.RawMessage(b)}
Defensive patterns

Strategy: type-guard

Validate before calling

func safeMetadata(v interface{}) (string, error) {
    switch v.(type) {
    case string, []byte, json.RawMessage, nil:
        // nil still fails NormalizeMetadataValue — use "{}" to clear
    default:
        b, err := json.Marshal(v)
        if err != nil { return "", err }
        return string(b), nil
    }
    return "", nil
}

Type guard

func isMetadataValue(v interface{}) bool {
    switch v.(type) {
    case string, []byte, json.RawMessage:
        return true
    default:
        return false
    }
}

Try / catch

if err != nil && strings.Contains(err.Error(), "metadata must be string, []byte, or json.RawMessage") {
    b, merr := json.Marshal(originalValue)
    if merr != nil { return merr }
    updates["metadata"] = json.RawMessage(b)
    err = retryUpdate(updates)
}

Prevention

When it happens

Trigger: Calling UpdateIssue with a "metadata" field set to map[string]interface{}, struct, nil, json.Number, or any type other than string/[]byte/json.RawMessage; also via ApplyMetadataEdits / CompareAndSet paths that funnel through NormalizeMetadataValue.

Common situations: Passing an already-decoded map from an API response instead of re-marshaling it; passing nil to clear metadata; upgrading code that previously accepted objects; building metadata values in tests with Go literals.

Related errors


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