gastownhall/beads · error

not a well-formed JSON value: %w

Error message

not a well-formed JSON value: %w

What it means

After the json.Valid fast path passes, CanonicalMetadataValue decodes the value with a decoder using UseNumber so numbers keep their source literal. If json.Decoder.Decode still fails (a rare race: input valid per json.Valid but rejected during decode, e.g. trailing garbage handling or decoder-level limits), this error wraps the underlying decoder error so the root cause is preserved via errors.Is/As.

Source

Thrown at internal/storage/metadata_cas.go:116

// ...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.
//
// Both sides are canonicalized here rather than assumed canonical, so it
// answers for raw caller input as well as for stored bytes.
func MetadataValuesEqual(a, b *json.RawMessage) (bool, error) {
	if a == nil || b == nil {
		return a == nil && b == nil, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap with errors.Is / %v inspection — the wrapped json.DecodeError names the exact offset and syntax problem.
  2. Re-marshal the value through json.Marshal or a fresh json.Decoder to normalize the bytes before canonicalizing.
  3. Round-trip: decode into any and re-marshal into json.RawMessage, dropping anything non-representable.
  4. If reproducible, report/inspect the input with truncateMetadataValue-style bounding and harden the producer.

Example fix

// before
canonical, err := storage.CanonicalMetadataValue(raw) // opaque decoder failure

// after
var probe any
if err := json.Unmarshal(raw, &probe); err != nil {
    return fmt.Errorf("metadata value undecodable: %w", err)
}
normalized, err := json.Marshal(probe)
if err != nil { return err }
canonical, err := storage.CanonicalMetadataValue(normalized)
Defensive patterns

Strategy: try-catch

Validate before calling

var probe any
if err := json.Unmarshal(raw, &probe); err != nil {
    return fmt.Errorf("metadata value undecodable: %w", err)
}

Try / catch

canonical, err := storage.CanonicalMetadataValue(raw)
if err != nil {
    var decErr *json.UnmarshalTypeError
    if errors.As(err, &decErr) {
        return fmt.Errorf("metadata value rejected at offset %d: %v", decErr.Offset, decErr)
    }
    return fmt.Errorf("canonicalizing metadata value: %w", err)
}

Prevention

When it happens

Trigger: Calling CanonicalMetadataValue / MetadataValuesEqual / CanonicalMetadataPointer (which backs PlanCompareAndSetKey) with bytes that pass json.Valid but fail Decode — practically: decoder-internal failure on pathological inputs; in practice this fires when json.Valid and the decoder disagree about the input.

Common situations: Exotic or corrupted byte sequences from a damaged store or a non-JSON-producing intermediary; a custom json.RawMessage source that mutated bytes between validation and decode; Go standard-library version differences in JSON acceptance.

Related errors


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