gastownhall/beads · error

existing metadata is not a JSON object: %w

Error message

existing metadata is not a JSON object: %w

What it means

MergeMetadataJSON wraps the json.Unmarshal error when the existing metadata blob cannot be decoded into a map[string]json.RawMessage. The library throws this because metadata merging only works on top-level JSON objects; empty, whitespace, or literal `null` existing values are tolerated, but anything else must be an object. The wrapped error (e.g. 'json: cannot unmarshal array into Go value of type map[string]json.RawMessage') names the actual offending JSON type.

Source

Thrown at internal/storage/metadata.go:255

// string-level unit test in metadata_jsonpath_test.go, not against the real
// SQL engines. Treat that escaping as defense-in-depth, not a proven
// contract, unless a caller starts passing unvalidated keys here.
func JSONMetadataPath(key string) string {
	return `$."` + jsonPathEscaper.Replace(key) + `"`
}

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

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the stored metadata (e.g. jq . on the JSON) and rewrite it as a top-level object: {"key":"value"}.
  2. If the blob is corrupted and unrecoverable, reset metadata to null or {} and re-apply the intended keys.
  3. Validate the JSON shape before calling: json.Valid + a quick unmarshal into map[string]json.RawMessage to confirm it is an object.
  4. Ensure the value you pass is raw JSON, not a pre-stringified JSON document wrapped in quotes.

Example fix

// before
existing := json.RawMessage(`["not","an","object"]`)
merged, err := MergeMetadataJSON(existing, incoming) // fails
// after
existing := json.RawMessage(`{"old":"value"}`)
merged, err := MergeMetadataJSON(existing, incoming)
Defensive patterns

Strategy: validation

Validate before calling

func isJSONObject(raw json.RawMessage) bool {
    t := strings.TrimSpace(string(raw))
    if t == "" || t == "null" {
        return true // tolerated as empty by MergeMetadataJSON
    }
    var m map[string]json.RawMessage
    return json.Unmarshal(raw, &m) == nil
}
if !isJSONObject(existing) { /* fix or reset metadata before merging */ }

Type guard

func asObject(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 {
    var syntaxErr *json.SyntaxError
    if errors.As(err, &syntaxErr) || strings.Contains(err.Error(), "not a JSON object") {
        // fall back: reset metadata to null and retry with just incoming
        merged, err = MergeMetadataJSON(nil, incoming)
    }
}

Prevention

When it happens

Trigger: Calling MergeMetadataJSON(existing, incoming) where existing is non-empty, non-null JSON that is not an object: an array ([1,2]), a string ("x"), a number (42), or malformed JSON. This typically comes from loading metadata stored by another tool/version into the --metadata-json merge path.

Common situations: Corrupted or hand-edited metadata stored in the issue row; a script that wrote a JSON array or scalar instead of an object; data migrated from an older schema where metadata had a different shape; piping the wrong file into --metadata-json.

Related errors


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