JuliusBrussee/caveman · error

invalid JSON number

Error message

invalid JSON number

What it means

normalizeJSONNumber rejects a literal consisting of only a minus sign with no following digits ('-'), which cannot be a valid JSON number. Although encoding/json's decoder normally guarantees valid grammar, this defensive check keeps the normalizer total for any literal path. The error is wrapped as 'proposalrun: canonicalize detail: %w' by CanonicalDetail.

Solutions

  1. Find where the detail blob is built and fix the producer so numbers are always complete digit sequences (never a lone '-').
  2. Check for buffer slicing/truncation bugs that could cut a JSON document mid-number.
  3. Validate the detail JSON with encoding/json before calling RowHash; a bare '-' fails json.Decode earlier with 'invalid character' so you get a clearer message.
  4. If you build json.Number values manually, validate with a regex like ^-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?$ first.

Example fix

// before
detail := []byte(`{"delta":` + signOnly + `}`) // signOnly == "-"

// after
if signOnly == "-" { signOnly = "0" }
detail := []byte(`{"delta":` + signOnly + `}`)
Defensive patterns

Strategy: validation

Validate before calling

var jsonNumberRe = regexp.MustCompile(`^-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?$`)
func validNumberLiteral(s string) bool { return jsonNumberRe.MatchString(s) }

Try / catch

canon, err := proposalrun.CanonicalDetail(detail)
if err != nil && strings.Contains(err.Error(), "invalid JSON number") {
    return fmt.Errorf("detail contains a malformed number literal: %w", err)
}

Prevention

When it happens

Trigger: A number token '-' (bare minus, no digits) reaching normalizeJSONNumber — normally only possible via a non-decoder code path or a corrupted json.Number built manually as json.Number("-").

Common situations: Code that constructs json.RawMessage/detail blobs by string concatenation and produces "-" as a value; manual json.Number("-") passed through a custom path; truncation bugs that cut a number mid-token (e.g. slicing a buffer at a bad offset).

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/7976c9697ce0a7fe. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/proposalrun/proposalrun.go:182

	}
	return canonicalJSONNumber(exact), nil
}

// normalizeJSONNumber converts one JSON number literal to the shortest plain
// decimal that has exactly the same base-10 value. It deliberately avoids
// floating-point conversion; all arithmetic is over digit counts and strings.
func normalizeJSONNumber(literal string) (string, error) {
	if len(literal) == 0 || len(literal) > maxJSONBNumberLexeme {
		return "", fmt.Errorf("number exceeds the supported JSONB numeric size")
	}
	start := 0
	negative := false
	if literal[0] == '-' {
		negative = true
		start = 1
	}
	if start == len(literal) {
		return "", fmt.Errorf("invalid JSON number")
	}

	mantissaEnd := len(literal)
	exponent := 0
	for i := start; i < len(literal); i++ {
		if literal[i] != 'e' && literal[i] != 'E' {
			continue
		}
		if mantissaEnd != len(literal) {
			return "", fmt.Errorf("invalid JSON number")
		}
		mantissaEnd = i
		var err error
		exponent, err = parseJSONExponent(literal[i+1:])
		if err != nil {
			return "", err
		}
	}

View on GitHub (pinned to 3ee70a1026)