gastownhall/beads · error

invalid %s: %w

Error message

invalid %s: %w

What it means

The metadata value supplied for an incremental merge operation failed normalization (storage.NormalizeMetadataValue). The error wraps the normalization failure under "invalid merge_metadata" (or the relevant op name), indicating the payload is not usable as metadata input.

Source

Thrown at internal/storage/issueops/update.go:887

// into a concrete "metadata" value on resolved, using oldIssue.Metadata (read in
// the same mutation transaction) as the base. It is a no-op when no metadata
// operation keys are present.
func resolveMetadataMergeOps(oldIssue *types.Issue, updates, resolved map[string]interface{}) error {
	_, hasMerge := updates[OpMergeMetadata]
	_, hasSet := updates[OpSetMetadata]
	_, hasUnset := updates[OpUnsetMetadata]
	if !hasMerge && !hasSet && !hasUnset {
		return nil
	}
	if _, direct := resolved["metadata"]; direct {
		return fmt.Errorf("cannot combine a metadata replacement with incremental metadata edits")
	}

	current := oldIssue.Metadata
	if hasMerge {
		normalized, err := storage.NormalizeMetadataValue(updates[OpMergeMetadata])
		if err != nil {
			return fmt.Errorf("invalid %s: %w", OpMergeMetadata, err)
		}
		merged, err := storage.MergeMetadataJSON(current, json.RawMessage(normalized))
		if err != nil {
			return fmt.Errorf("metadata merge failed: %w", err)
		}
		current = merged
	}
	if hasSet || hasUnset {
		unset, err := mergeOpStrings(OpUnsetMetadata, updates[OpUnsetMetadata], hasUnset)
		if err != nil {
			return err
		}
		var merged json.RawMessage
		if set, typed := updates[OpSetMetadata].(map[string]json.RawMessage); typed {
			merged, err = applyTypedMetadataEdits(current, set, unset)
		} else {
			var set []string
			set, err = mergeOpStrings(OpSetMetadata, updates[OpSetMetadata], hasSet)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Validate the merge payload is a JSON object before submitting (json.Valid + unmarshal into map[string]any)
  2. Read the wrapped %w cause for the exact normalization problem
  3. Convert array/scalar payloads into key/value objects

Example fix

// before
updates[issueops.OpMergeMetadata] = []string{"a", "b"} // not an object

// after
updates[issueops.OpMergeMetadata] = map[string]any{"tags": []string{"a", "b"}}
Defensive patterns

Strategy: validation

Validate before calling

func validMergePayload(v any) error {
    b, err := json.Marshal(v)
    if err != nil { return err }
    var obj map[string]any
    if err := json.Unmarshal(b, &obj); err != nil {
        return fmt.Errorf("merge payload must be a JSON object: %w", err)
    }
    return nil
}

Type guard

func isJSONObject(raw json.RawMessage) bool {
    var m map[string]json.RawMessage
    return json.Unmarshal(raw, &m) == nil
}

Try / catch

if err := storage.NormalizeMetadataValue(updates[issueops.OpMergeMetadata]); err != nil {
    return fmt.Errorf("rejecting update: merge payload invalid: %w", err)
}

Prevention

When it happens

Trigger: Passing OpMergeMetadata with a value that NormalizeMetadataValue rejects — e.g. non-JSON-serializable input, a non-object JSON document (array/string/number), or invalid types inside the object.

Common situations: Passing a JSON array or scalar where an object is expected, feeding user input that was never JSON-validated, or a CLI flag carrying malformed JSON.

Related errors


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