gastownhall/beads · error

failed to marshal metadata: %w

Error message

failed to marshal metadata: %w

What it means

After applying metadata set/unset edits, re-marshaling the resulting map into a JSON document failed. This is nearly impossible with well-formed content (map[string]json.RawMessage marshals reliably) and usually indicates corrupted in-memory state or an unexpected value type in the map.

Source

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

	sort.Strings(keys)
	for _, key := range keys {
		if err := storage.ValidateMetadataKey(key); err != nil {
			return nil, err
		}
		if !json.Valid(set[key]) {
			return nil, fmt.Errorf("metadata value for key %q is not valid JSON", key)
		}
		data[key] = set[key]
	}
	for _, key := range unset {
		if err := storage.ValidateMetadataKey(key); err != nil {
			return nil, err
		}
		delete(data, key)
	}
	result, err := json.Marshal(data)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal metadata: %w", err)
	}
	return json.RawMessage(result), nil
}

// resolveNotesAppendOp folds OpAppendNotes into a concrete "notes" value on
// resolved, appending to oldIssue.Notes (read in the same mutation transaction).
// It is a no-op when the append op is absent.
func resolveNotesAppendOp(oldIssue *types.Issue, updates, resolved map[string]interface{}) error {
	raw, ok := updates[OpAppendNotes]
	if !ok {
		return nil
	}
	if _, direct := resolved["notes"]; direct {
		return fmt.Errorf("%w: cannot combine a notes replacement with %s", storage.ErrValidation, OpAppendNotes)
	}
	text, ok := raw.(string)
	if !ok {
		return fmt.Errorf("%s must be a string, got %T", OpAppendNotes, raw)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure all values stored in the metadata map are valid json.RawMessage entries
  2. Retry the operation; a transient memory/state issue is unlikely but safe to re-run
  3. If reproducible, dump the offending map and file a bug — valid RawMessage maps should always marshal

Example fix

// before
data["k"] = rawMessageFromUntrustedSource // may be non-JSON bytes

// after
if !json.Valid(rawMessageFromUntrustedSource) {
    return fmt.Errorf("invalid metadata value")
}
data["k"] = rawMessageFromUntrustedSource
Defensive patterns

Strategy: validation

Validate before calling

result, err := json.Marshal(data)
if err != nil {
    return fmt.Errorf("pre-flight marshal failed: %w", err)
}

Type guard

func marshalable(m map[string]json.RawMessage) bool {
    _, err := json.Marshal(m)
    return err == nil
}

Try / catch

if err := issueops.ResolveMergeOps(issue, updates, resolved); err != nil {
    if strings.Contains(err.Error(), "failed to marshal metadata") {
        return fmt.Errorf("metadata map corrupted; rebuild from source data: %w", err)
    }
}

Prevention

When it happens

Trigger: applyTypedMetadataEdits finishing with a data map that json.Marshal cannot serialize — practically only when invalid entries were injected or memory/state is corrupted.

Common situations: Custom tooling mutating the map between edit and marshal, or embedding values json cannot handle if the map type was widened beyond json.RawMessage.

Related errors


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