Skip to content

decoder: reject malformed number literals to match encoding/json - #598

Open
sueun-dev wants to merge 3 commits into
goccy:masterfrom
sueun-dev:fix-number-grammar-validation
Open

sueun-dev wants to merge 3 commits into
goccy:masterfrom
sueun-dev:fix-number-grammar-validation

Conversation

@sueun-dev

@sueun-dev sueun-dev commented Jul 29, 2026

Copy link
Copy Markdown

go-json is a drop-in replacement for encoding/json, but value decoding accepted several malformed number literals that encoding/json rejects:

json.Unmarshal([]byte("01"), &v)    // go-json: ok (1);    encoding/json: error
json.Unmarshal([]byte("1."), &v)    // go-json: ok (1);    encoding/json: error
json.Unmarshal([]byte("-.5"), &v)   // go-json: ok (-0.5); encoding/json: error
json.Valid([]byte("00e1"))          // go-json: true;      encoding/json: false

The same grammar gap also showed up while skipping unknown struct fields. In #448, {"fake": 0.} unmarshaled into struct{} returned nil in go-json while encoding/json rejected it.

The parser was delimiting a candidate number by greedily consuming digit/sign/dot/exponent bytes and then relying on strconv.ParseFloat, which is looser than the JSON grammar. ParseFloat accepts leading zeros (01, 007, 00.5), bare trailing dots (0., 1., 1.e5), and a fraction with no integer part after a sign (-.5).

Fix: add validNumber (a port of encoding/json's isValidNumber) and check it after ParseFloat in the float and json.Number decoders. Also use the same check while skipping numeric values in unknown fields. The skipped array/object path now parses value boundaries instead of only balancing delimiters, so malformed numeric values are not hidden inside skipped composites.

I left Decoder.Token() unchanged; it is a separate token-stream API, and encoding/json.Decoder.Token() can also return the first token for inputs like 00.

This also re-enables five cases in the existing TestNumberIsValid that were commented out in the commit that added the test (c9f1d00) because go-json accepted them: 01, 1., 012, 01.2, 1.e1.

Fixes #448.

Testing:

  • go test ./... -run 'TestDecodeFirstWin|TestUnmarshal(Rejects|Accepts).*NumberLiterals|TestNumberIsValid|TestDecoderValidatesSkippedNumberAcrossBuffer' -count=1 -v
  • go test ./... -count=1
  • GOGC=1 go test ./... -count=1
  • go test -race ./... -count=1
  • GOTOOLCHAIN=go1.19.13 CGO_ENABLED=0 go test ./... -count=1
  • GOTOOLCHAIN=go1.20.14 CGO_ENABLED=0 go test ./... -count=1
  • GOTOOLCHAIN=go1.21.13 CGO_ENABLED=0 go test ./... -count=1
  • make cover
  • cd benchmarks && go test -run '^$' -bench GoJson -benchtime=1x
  • official prebuilt golangci-lint v1.54.2: golangci-lint run --concurrency=1 --timeout=5m
  • gofmt -l internal/decoder/context.go internal/decoder/stream.go number_validation_test.go produced no output

The number decoders delimit a number token by greedily consuming digit/
sign/dot/exponent bytes and then validate only with strconv.ParseFloat,
which is more permissive than the JSON grammar (RFC 8259). As a result
go-json accepted literals encoding/json rejects: a leading zero (01, 007,
00.5), a bare trailing dot (1., 1.e5), and a fraction with no integer part
after a sign (-.5). This breaks the drop-in encoding/json compatibility the
package targets, and diverges Valid() from encoding/json.Valid().

Add validNumber (a port of encoding/json's isValidNumber) and check it in the
float and json.Number decoders after ParseFloat has already accepted the
token, so no previously-rejected input changes behavior; the check only closes
the gap where ParseFloat is looser than JSON. Covers Unmarshal into
interface{}/float/json.Number and, through the interface stream decoder, Valid.
…idation

These five encoding/json invalidTests entries (01, 1., 012, 01.2, 1.e1) were
commented out when the test was added (c9f1d00) because go-json accepted them.
The number-validation fix now rejects them, so restore the cases.
Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The behavior of the syntax check for skipped numeric literals is different from the standard library

1 participant