gastownhall/beads · error
not a well-formed JSON value: %q
Error message
not a well-formed JSON value: %q
What it means
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.
Source
Thrown at internal/storage/metadata_cas.go:110
//
// NUMBERS KEEP THEIR SOURCE LITERAL, so 1 and 1.0 canonicalize differently and
// do not match. This function does not round-trip a number through float64, and
// the reason is NOT that doing so would lose precision the store keeps: the
// metadata column loses it first. go-mysql-server decodes JSON numbers into
// float64 and re-emits them, measured — 9007199254740993 is stored as
// ...992, 1.0 as 1, -0.0 as 0, 1e300 as three hundred and one digits. So the
// substrate's own fidelity, not this rule, is what bounds a numeric value.
//
// What the literal rule buys is that this function stays a pure statement about
// JSON rather than a copy of one engine's number handling — a copy that would
// silently equate two values a TEXT-column backend can hold apart, on the one
// comparison a compare-and-set exists to make. What it COSTS is that a caller
// composing an expectation from its own spelling of a number can disagree with
// the row; the role answers that by making Current the value the ROW holds, so
// the documented loop converges. See issueops.CompareAndSetKeyRequest.Expected.
func CanonicalMetadataValue(raw json.RawMessage) (json.RawMessage, error) {
if !json.Valid(raw) {
return nil, fmt.Errorf("not a well-formed JSON value: %q", truncateMetadataValue(raw))
}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
var value any
if err := dec.Decode(&value); err != nil {
return nil, fmt.Errorf("not a well-formed JSON value: %w", err)
}
var buf bytes.Buffer
if err := writeCanonicalMetadataJSON(&buf, value); err != nil {
return nil, err
}
return json.RawMessage(buf.Bytes()), nil
}
// MetadataValuesEqual reports whether two optional metadata values are the same
// value under the canonical rule, with nil meaning ABSENT on either side. An
// absent key equals only an absent key: a key stored holding JSON null is
// present, and the metadata object can show the difference.View on GitHub (pinned to 71377f2769)
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.
Example fix
// before
raw := json.RawMessage(userInput) // may be any text
_, err := storage.CanonicalMetadataValue(raw)
// after
if !json.Valid(raw) {
return fmt.Errorf("metadata value is not valid JSON: %s", userInput)
}
canonical, err := storage.CanonicalMetadataValue(raw) Defensive patterns
Strategy: validation
Validate before calling
if raw == nil || !json.Valid(raw) {
return fmt.Errorf("refusing non-JSON metadata value: %q", raw)
} Type guard
func isWellFormedJSON(raw json.RawMessage) bool { return len(raw) > 0 && json.Valid(raw) } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ExternalDoltConfig: must set Socket or (Host, Port)
- metadata.repo: %w
- metadata.repo must not be null
- loading config for gate resolution: %w
- invalid metadata: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/07b9f7efb754e394.
Report an issue: GitHub.