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 withView on GitHub (pinned to 71377f2769)
Solutions
- Marshal the metadata to valid JSON before passing it: json.Marshal(v) and pass the resulting string.
- Validate the JSON parses (json.Valid) before calling UpdateIssue.
- Check the API contract — metadata must normalize to a JSON object/string.
- 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
- Always json.Marshal structured metadata before UpdateIssue.
- Run json.Valid on metadata strings before writes.
- Centralize metadata construction in one helper.
- Keep metadata values as JSON objects, not free text.
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
- ExternalDoltConfig: must set Socket or (Host, Port)
- metadata.repo: %w
- metadata.repo must not be null
- loading config for gate resolution: %w
- invalid metadata: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/a78ddf8fee1d41df.
Report an issue: GitHub.