{"record":{"id":"2b525fd0ac402932","repo":"gastownhall/beads","slug":"existing-metadata-is-not-a-json-object-w-2b525f","errorCode":null,"errorMessage":"existing metadata is not a JSON object: %w","messagePattern":"existing metadata is not a JSON object: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/metadata.go","lineNumber":255,"sourceCode":"// string-level unit test in metadata_jsonpath_test.go, not against the real\n// SQL engines. Treat that escaping as defense-in-depth, not a proven\n// contract, unless a caller starts passing unvalidated keys here.\nfunc JSONMetadataPath(key string) string {\n\treturn `$.\"` + jsonPathEscaper.Replace(key) + `\"`\n}\n\nvar jsonPathEscaper = strings.NewReplacer(`\\`, `\\\\`, `\"`, `\\\"`)\n\n// MergeMetadataJSON merges incoming metadata JSON into existing metadata.\n// Top-level keys from incoming overwrite keys in existing; keys only in\n// existing are preserved. Both inputs must be JSON objects (or empty/null).\nfunc MergeMetadataJSON(existing, incoming json.RawMessage) (json.RawMessage, error) {\n\tbase := make(map[string]json.RawMessage)\n\tif len(existing) > 0 {\n\t\ttrimmed := strings.TrimSpace(string(existing))\n\t\tif trimmed != \"\" && trimmed != \"null\" {\n\t\t\tif err := json.Unmarshal(existing, &base); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"existing metadata is not a JSON object: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\toverlay := make(map[string]json.RawMessage)\n\tif err := json.Unmarshal(incoming, &overlay); err != nil {\n\t\treturn nil, fmt.Errorf(\"new metadata is not a JSON object: %w\", err)\n\t}\n\n\tfor k, v := range overlay {\n\t\tbase[k] = v\n\t}\n\n\tresult, err := json.Marshal(base)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal merged metadata: %w\", err)\n\t}\n\treturn json.RawMessage(result), nil","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata.go#L237-L273","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the stored metadata (e.g. jq . on the JSON) and rewrite it as a top-level object: {\"key\":\"value\"}.","If the blob is corrupted and unrecoverable, reset metadata to null or {} and re-apply the intended keys.","Validate the JSON shape before calling: json.Valid + a quick unmarshal into map[string]json.RawMessage to confirm it is an object.","Ensure the value you pass is raw JSON, not a pre-stringified JSON document wrapped in quotes."],"exampleFix":"// before\nexisting := json.RawMessage(`[\"not\",\"an\",\"object\"]`)\nmerged, err := MergeMetadataJSON(existing, incoming) // fails\n// after\nexisting := json.RawMessage(`{\"old\":\"value\"}`)\nmerged, err := MergeMetadataJSON(existing, incoming)","handlingStrategy":"validation","validationCode":"func isJSONObject(raw json.RawMessage) bool {\n    t := strings.TrimSpace(string(raw))\n    if t == \"\" || t == \"null\" {\n        return true // tolerated as empty by MergeMetadataJSON\n    }\n    var m map[string]json.RawMessage\n    return json.Unmarshal(raw, &m) == nil\n}\nif !isJSONObject(existing) { /* fix or reset metadata before merging */ }","typeGuard":"func asObject(raw json.RawMessage) (map[string]json.RawMessage, bool) {\n    var m map[string]json.RawMessage\n    if err := json.Unmarshal(raw, &m); err != nil {\n        return nil, false\n    }\n    return m, true\n}","tryCatchPattern":"merged, err := MergeMetadataJSON(existing, incoming)\nif err != nil {\n    var syntaxErr *json.SyntaxError\n    if errors.As(err, &syntaxErr) || strings.Contains(err.Error(), \"not a JSON object\") {\n        // fall back: reset metadata to null and retry with just incoming\n        merged, err = MergeMetadataJSON(nil, incoming)\n    }\n}","preventionTips":["Always write metadata as a top-level JSON object; never store arrays or bare scalars.","Run jq -e 'type == \"object\"' on metadata payloads before importing them.","Keep an eye on schema changes when migrating from older versions or other tools."],"tags":["json","metadata","validation"],"backgroundTag":"metadata-not-json-object","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}