{"record":{"id":"07b9f7efb754e394","repo":"gastownhall/beads","slug":"not-a-well-formed-json-value-q","errorCode":null,"errorMessage":"not a well-formed JSON value: %q","messagePattern":"not a well-formed JSON value: %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/metadata_cas.go","lineNumber":110,"sourceCode":"//\n// NUMBERS KEEP THEIR SOURCE LITERAL, so 1 and 1.0 canonicalize differently and\n// do not match. This function does not round-trip a number through float64, and\n// the reason is NOT that doing so would lose precision the store keeps: the\n// metadata column loses it first. go-mysql-server decodes JSON numbers into\n// float64 and re-emits them, measured — 9007199254740993 is stored as\n// ...992, 1.0 as 1, -0.0 as 0, 1e300 as three hundred and one digits. So the\n// substrate's own fidelity, not this rule, is what bounds a numeric value.\n//\n// What the literal rule buys is that this function stays a pure statement about\n// JSON rather than a copy of one engine's number handling — a copy that would\n// silently equate two values a TEXT-column backend can hold apart, on the one\n// comparison a compare-and-set exists to make. What it COSTS is that a caller\n// composing an expectation from its own spelling of a number can disagree with\n// the row; the role answers that by making Current the value the ROW holds, so\n// the documented loop converges. See issueops.CompareAndSetKeyRequest.Expected.\nfunc CanonicalMetadataValue(raw json.RawMessage) (json.RawMessage, error) {\n\tif !json.Valid(raw) {\n\t\treturn nil, fmt.Errorf(\"not a well-formed JSON value: %q\", truncateMetadataValue(raw))\n\t}\n\tdec := json.NewDecoder(bytes.NewReader(raw))\n\tdec.UseNumber()\n\tvar value any\n\tif err := dec.Decode(&value); err != nil {\n\t\treturn nil, fmt.Errorf(\"not a well-formed JSON value: %w\", err)\n\t}\n\tvar buf bytes.Buffer\n\tif err := writeCanonicalMetadataJSON(&buf, value); err != nil {\n\t\treturn nil, err\n\t}\n\treturn json.RawMessage(buf.Bytes()), nil\n}\n\n// MetadataValuesEqual reports whether two optional metadata values are the same\n// value under the canonical rule, with nil meaning ABSENT on either side. An\n// absent key equals only an absent key: a key stored holding JSON null is\n// present, and the metadata object can show the difference.","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata_cas.go#L92-L128","documentation":"CanonicalMetadataValue returns raw's canonical encoding (whitespace removed, object keys sorted, numbers kept as source literals) — the single definition of metadata-value equality for compare-and-set. Before decoding it first checks json.Valid; if the raw bytes are not well-formed JSON it refuses with this error, quoting a truncated (64-byte-bounded) form of the input so a huge junk value does not become a huge error message.","triggerScenarios":"Calling CanonicalMetadataValue directly, or indirectly via CanonicalMetadataPointer (from PlanCompareAndSetKey) or MetadataValuesEqual, with a *json.RawMessage / json.RawMessage whose bytes fail json.Valid — empty input, truncated JSON, stray text, or double-encoded values like `\"{\\\"a\\\":1}\"` treated as an object.","commonSituations":"Reconstructing stored metadata from a TEXT column or log line that was mangled; comparing against an expectation string written by hand; a backend returning a quoted JSON string where an object was expected; empty bytes representing an 'unset' value instead of nil.","solutions":["Inspect the quoted bytes in the error message (truncated at 64 chars with an ellipsis) to see what malformed input was actually passed.","Fix the producer of the bytes to emit valid JSON — prefer json.Marshal over hand-built strings.","Call json.Valid(raw) as a pre-check at the API boundary and reject early.","If the value is genuinely absent, pass nil (*json.RawMessage) — CanonicalMetadataPointer treats nil as ABSENT and skips canonicalization."],"exampleFix":"// before\nraw := json.RawMessage(userInput) // may be any text\n_, err := storage.CanonicalMetadataValue(raw)\n\n// after\nif !json.Valid(raw) {\n    return fmt.Errorf(\"metadata value is not valid JSON: %s\", userInput)\n}\ncanonical, err := storage.CanonicalMetadataValue(raw)","handlingStrategy":"validation","validationCode":"if raw == nil || !json.Valid(raw) {\n    return fmt.Errorf(\"refusing non-JSON metadata value: %q\", raw)\n}","typeGuard":"func isWellFormedJSON(raw json.RawMessage) bool { return len(raw) > 0 && json.Valid(raw) }","tryCatchPattern":null,"preventionTips":["Validate with json.Valid at the boundary where raw bytes enter your system.","Never treat an empty byte slice as a value; use nil to mean absent.","Beware double-encoded strings: `\"{\\\"a\\\":1}\"` is a valid JSON string, not an object — decode intentionally.","Prefer a single serialization path (json.Marshal/Encoder) for anything stored as metadata."],"tags":["json","validation","metadata","canonicalization"],"backgroundTag":"invalid-json-metadata-value","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}