gastownhall/beads · error

new metadata is not a JSON object: %w

Error message

new metadata is not a JSON object: %w

What it means

MergeMetadataJSON wraps the json.Unmarshal error when the incoming (overlay) metadata cannot be decoded into a JSON object map. Unlike the existing side, incoming is not given the empty/null allowance in the source shown, so any incoming value that is not a JSON object — array, scalar, quoted string, or malformed JSON — is rejected. Incoming keys overwrite existing ones, so the overlay must always be an object.

Source

Thrown at internal/storage/metadata.go:262

var jsonPathEscaper = strings.NewReplacer(`\`, `\\`, `"`, `\"`)

// MergeMetadataJSON merges incoming metadata JSON into existing metadata.
// Top-level keys from incoming overwrite keys in existing; keys only in
// existing are preserved. Both inputs must be JSON objects (or empty/null).
func MergeMetadataJSON(existing, incoming json.RawMessage) (json.RawMessage, error) {
	base := make(map[string]json.RawMessage)
	if len(existing) > 0 {
		trimmed := strings.TrimSpace(string(existing))
		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)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure the incoming argument is a top-level JSON object: {"key":"value"}, not ["..."] or a bare scalar.
  2. Check for double encoding: if the value starts with \" or is escaped JSON, decode one layer first.
  3. Validate with json.Unmarshal([]byte(in), &map[string]json.RawMessage{}) in a pre-check before the call.
  4. If passing via CLI, quote the whole JSON argument so the shell does not split or strip braces.

Example fix

// before
incoming := json.RawMessage(`"{\"k\":\"v\"}"`) // double-encoded string
merged, err := MergeMetadataJSON(existing, incoming) // fails
// after
incoming := json.RawMessage(`{"k":"v"}`)
merged, err := MergeMetadataJSON(existing, incoming)
Defensive patterns

Strategy: validation

Validate before calling

func validOverlay(raw json.RawMessage) bool {
    var m map[string]json.RawMessage
    return len(raw) > 0 && json.Unmarshal(raw, &m) == nil
}
if !validOverlay(incoming) { /* fix the payload before calling MergeMetadataJSON */ }

Type guard

func asOverlay(raw json.RawMessage) (map[string]json.RawMessage, bool) {
    var m map[string]json.RawMessage
    if err := json.Unmarshal(raw, &m); err != nil {
        return nil, false
    }
    return m, true
}

Try / catch

merged, err := MergeMetadataJSON(existing, incoming)
if err != nil && strings.Contains(err.Error(), "new metadata is not a JSON object") {
    // decode one layer if the payload was double-encoded
    var s string
    if json.Unmarshal(incoming, &s) == nil {
        merged, err = MergeMetadataJSON(existing, json.RawMessage(s))
    }
}

Prevention

When it happens

Trigger: Calling MergeMetadataJSON(existing, incoming) with incoming = []byte(`"string"`), an array, a number, `null` (unmarshal into map fails with cannot unmarshal), or truncated JSON. Happens when a CLI builds the overlay from --metadata flags or external JSON that was not normalized.

Common situations: Passing a JSON array of key/value pairs instead of an object; double-encoding the payload (a JSON string containing JSON); shell quoting mangling the --metadata-json argument so it arrives truncated or quoted.

Related errors


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