gastownhall/beads · error

failed to marshal merged metadata: %w

Error message

failed to marshal merged metadata: %w

What it means

After merging, MergeMetadataJSON marshals the combined map[string]json.RawMessage back to JSON; this error wraps any json.Marshal failure. It is rare because the map was just populated by successfully-unmarshaled JSON; it mainly fires when a stored value inside the map is itself invalid raw JSON. The library throws it as a safety net rather than writing corrupt metadata.

Source

Thrown at internal/storage/metadata.go:271

		if trimmed != "" && trimmed != "null" {
			if err := json.Unmarshal(existing, &base); err != nil {
				return nil, fmt.Errorf("existing metadata is not a JSON object: %w", err)
			}
		}
	}

	overlay := make(map[string]json.RawMessage)
	if err := json.Unmarshal(incoming, &overlay); err != nil {
		return nil, fmt.Errorf("new metadata is not a JSON object: %w", err)
	}

	for k, v := range overlay {
		base[k] = v
	}

	result, err := json.Marshal(base)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal merged metadata: %w", err)
	}
	return json.RawMessage(result), nil
}

// ApplyMetadataEdits applies incremental set (key=value) and unset (key) edits
// to existing metadata and returns the merged JSON. Set values are typed via
// MetadataEditValue; keys are validated with ValidateMetadataKey.
func ApplyMetadataEdits(existing json.RawMessage, setFlags, unsetFlags []string) (json.RawMessage, error) {
	data := make(map[string]json.RawMessage)
	if len(existing) > 0 {
		trimmed := strings.TrimSpace(string(existing))
		if trimmed != "" && trimmed != "null" {
			if err := json.Unmarshal(existing, &data); err != nil {
				return nil, fmt.Errorf("existing metadata is not a JSON object: %w", err)
			}
		}
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Validate every raw value fragment is itself valid JSON (json.Valid) before composing the map.
  2. Re-obtain the existing metadata from storage instead of reusing a mutated copy.
  3. If programmatically building values, produce them with json.Marshal, never string concatenation.
  4. Report upstream if it reproduces on data produced solely by MergeMetadataJSON — that indicates corrupt stored metadata.

Example fix

// before
base["cfg"] = json.RawMessage(`{broken`) // invalid fragment
merged, err := MergeMetadataJSON(existing, incoming)
// after
v, _ := json.Marshal(map[string]string{"a": "b"})
base["cfg"] = v
merged, err := MergeMetadataJSON(existing, incoming)
Defensive patterns

Strategy: try-catch

Validate before calling

for k, v := range fragments {
    if !json.Valid(v) {
        return fmt.Errorf("fragment %q is not valid JSON", k)
    }
} // run on any hand-built json.RawMessage map before merging

Type guard

func validRaw(v json.RawMessage) bool { return json.Valid(v) }

Try / catch

result, err := MergeMetadataJSON(existing, incoming)
if err != nil && strings.Contains(err.Error(), "failed to marshal merged metadata") {
    // stored data is corrupt: start fresh from incoming only
    result, err = MergeMetadataJSON(nil, incoming)
}

Prevention

When it happens

Trigger: The merged base map contains a json.RawMessage value that is not valid JSON (possible when existing was decoded loosely or constructed manually and then re-fed), or an out-of-memory/unsupported-value edge during Marshal.

Common situations: Re-merging output that was produced by an external tool and partially rewritten; programmatically composing json.RawMessage values by hand with malformed fragments; extremely deep nesting hitting encoder limits.

Related errors


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