gastownhall/beads · error

invalid metadata: %w

Error message

invalid metadata: %w

What it means

UpdateIssue validates the 'metadata' update value before routing to SQL. storage.NormalizeMetadataValue coerces/normalizes the raw value to a JSON string; any failure (non-string, non-JSON-serializable value) is wrapped as 'invalid metadata'. This is a pre-flight input validation error; nothing was written.

Source

Thrown at internal/storage/embeddeddolt/issues.go:64

// UnclaimIssueIfAssignee releases a claim only while the issue is still assigned
// to expectedAssignee (compare-and-swap, the inverse of ClaimIssue). Returns
// storage.ErrAssigneeMismatch, leaving the issue untouched, when the current
// assignee differs. Delegates SQL work to issueops; EmbeddedDolt auto-commits
// the transaction.
func (s *EmbeddedDoltStore) UnclaimIssueIfAssignee(ctx context.Context, id string, actor string, expectedAssignee string) error {
	return s.withConn(ctx, true, func(tx *sql.Tx) error {
		return issueops.UnclaimIssueIfAssigneeInTx(ctx, tx, id, actor, expectedAssignee)
	})
}

// UpdateIssue updates fields on an issue.
// Delegates SQL work to issueops; EmbeddedDolt auto-commits the transaction.
func (s *EmbeddedDoltStore) UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
	// Validate metadata against schema before routing.
	if rawMeta, ok := updates["metadata"]; ok {
		metadataStr, err := storage.NormalizeMetadataValue(rawMeta)
		if err != nil {
			return fmt.Errorf("invalid metadata: %w", err)
		}
		if err := issueops.ValidateMetadataIfConfigured(json.RawMessage(metadataStr)); err != nil {
			return err
		}
	}

	return s.withConn(ctx, true, func(tx *sql.Tx) error {
		_, err := issueops.UpdateIssueInTx(ctx, tx, id, updates, actor)
		return err
	})
}

// UpdateIssueChecked applies the update like UpdateIssue, adding optional
// atomic preconditions: when opts.ExpectedVersion is non-nil the update
// proceeds only if the issue's current RowVersion (row_lock) still equals
// *opts.ExpectedVersion, else it refuses with storage.ErrVersionMismatch; when
// opts.ExpectedAssignee/ExpectedStatus are non-nil the update proceeds only if
// the issue's current assignee/status match, else it refuses with

View on GitHub (pinned to 71377f2769)

Solutions

  1. Marshal the metadata to valid JSON before passing it: json.Marshal(v) and pass the resulting string.
  2. Validate the JSON parses (json.Valid) before calling UpdateIssue.
  3. Check the API contract — metadata must normalize to a JSON object/string.
  4. If validation still fails, run the value through issueops.ValidateMetadataIfConfigured locally to see the schema complaint.

Example fix

// before: raw struct -> invalid metadata
updates := map[string]interface{}{"metadata": myMetaStruct}
store.UpdateIssue(ctx, id, updates, actor)
// after: marshal to JSON string first
b, _ := json.Marshal(myMetaStruct)
updates := map[string]interface{}{"metadata": string(b)}
store.UpdateIssue(ctx, id, updates, actor)
Defensive patterns

Strategy: validation

Validate before calling

func validMetadata(v interface{}) (string, error) {
    b, err := json.Marshal(v)
    if err != nil { return "", err }
    if !json.Valid(b) { return "", fmt.Errorf("metadata is not valid JSON") }
    return string(b), nil
}

Type guard

func isMetadataString(v interface{}) (string, bool) {
    s, ok := v.(string)
    return s, ok && json.Valid([]byte(s))
}

Try / catch

err := store.UpdateIssue(ctx, id, updates, actor)
if err != nil && strings.Contains(err.Error(), "invalid metadata") {
    // nothing was written; fix the value and retry
    if b, merr := json.Marshal(updates["metadata"]); merr == nil {
        updates["metadata"] = string(b)
        err = store.UpdateIssue(ctx, id, updates, actor)
    }
}

Prevention

When it happens

Trigger: Calling store.UpdateIssue(ctx, id, map[string]interface{}{"metadata": <bad value>}, actor) where the metadata value is not a string and not JSON-marshalable — e.g. a struct with unexported fields, a channel, or a func.

Common situations: Callers passing a Go map/struct instead of a JSON string; hand-built JSON with a syntax error; middleware that mangled the value type; older clients sending plain text metadata.

Related errors


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