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

  1. Inspect the quoted bytes in the error message (truncated at 64 chars with an ellipsis) to see what malformed input was actually passed.
  2. Fix the producer of the bytes to emit valid JSON — prefer json.Marshal over hand-built strings.
  3. Call json.Valid(raw) as a pre-check at the API boundary and reject early.
  4. 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

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


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