From: Joe Tsai Date: Mon, 6 Oct 2025 19:56:29 +0000 (-0700) Subject: encoding/json: fix regression in quoted numbers under goexperiment.jsonv2 X-Git-Tag: go1.26rc1~638 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=b497a29d25;p=gostls13.git encoding/json: fix regression in quoted numbers under goexperiment.jsonv2 The legacy parsing of quoted numbers in v1 was according to the Go grammar for a number, rather than the JSON grammar for a number. The former is a superset of the latter. This is a historical mistake, but usages exist that depend on it. We already have branches for StringifyWithLegacySemantics to handle quoted nulls, so we can expand it to handle this. Fixes #75619 Change-Id: Ic07802539b7cbe0e1f53bd0f7e9bb344a8447203 Reviewed-on: https://go-review.googlesource.com/c/go/+/709615 Reviewed-by: Damien Neil Auto-Submit: Joseph Tsai LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Pratt --- diff --git a/src/encoding/json/decode_test.go b/src/encoding/json/decode_test.go index d12495f90b..0b26b8eb91 100644 --- a/src/encoding/json/decode_test.go +++ b/src/encoding/json/decode_test.go @@ -1237,6 +1237,62 @@ var unmarshalTests = []struct { out: (chan int)(nil), err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[chan int](), Offset: 1}, }, + + // #75619 + { + CaseName: Name("QuotedInt/GoSyntax"), + in: `{"X": "-0000123"}`, + ptr: new(struct { + X int64 `json:",string"` + }), + out: struct { + X int64 `json:",string"` + }{-123}, + }, + { + CaseName: Name("QuotedInt/Invalid"), + in: `{"X": "123 "}`, + ptr: new(struct { + X int64 `json:",string"` + }), + err: &UnmarshalTypeError{Value: "number 123 ", Type: reflect.TypeFor[int64](), Field: "X", Offset: int64(len(`{"X": "123 "`))}, + }, + { + CaseName: Name("QuotedUint/GoSyntax"), + in: `{"X": "0000123"}`, + ptr: new(struct { + X uint64 `json:",string"` + }), + out: struct { + X uint64 `json:",string"` + }{123}, + }, + { + CaseName: Name("QuotedUint/Invalid"), + in: `{"X": "0x123"}`, + ptr: new(struct { + X uint64 `json:",string"` + }), + err: &UnmarshalTypeError{Value: "number 0x123", Type: reflect.TypeFor[uint64](), Field: "X", Offset: int64(len(`{"X": "0x123"`))}, + }, + { + CaseName: Name("QuotedFloat/GoSyntax"), + in: `{"X": "0x1_4p-2"}`, + ptr: new(struct { + X float64 `json:",string"` + }), + out: struct { + X float64 `json:",string"` + }{0x1_4p-2}, + }, + { + CaseName: Name("QuotedFloat/Invalid"), + in: `{"X": "1.5e1_"}`, + ptr: new(struct { + X float64 `json:",string"` + }), + err: &UnmarshalTypeError{Value: "number 1.5e1_", Type: reflect.TypeFor[float64](), Field: "X", Offset: int64(len(`{"X": "1.5e1_"`))}, + }, } func TestMarshal(t *testing.T) { diff --git a/src/encoding/json/v2/arshal_default.go b/src/encoding/json/v2/arshal_default.go index c2307fa31d..078d345e14 100644 --- a/src/encoding/json/v2/arshal_default.go +++ b/src/encoding/json/v2/arshal_default.go @@ -474,10 +474,21 @@ func makeIntArshaler(t reflect.Type) *arshaler { break } val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) - if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) && string(val) == "null" { - if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { - va.SetInt(0) + if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) { + // For historical reasons, v1 parsed a quoted number + // according to the Go syntax and permitted a quoted null. + // See https://go.dev/issue/75619 + n, err := strconv.ParseInt(string(val), 10, bits) + if err != nil { + if string(val) == "null" { + if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { + va.SetInt(0) + } + return nil + } + return newUnmarshalErrorAfterWithValue(dec, t, errors.Unwrap(err)) } + va.SetInt(n) return nil } fallthrough @@ -561,10 +572,21 @@ func makeUintArshaler(t reflect.Type) *arshaler { break } val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) - if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) && string(val) == "null" { - if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { - va.SetUint(0) + if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) { + // For historical reasons, v1 parsed a quoted number + // according to the Go syntax and permitted a quoted null. + // See https://go.dev/issue/75619 + n, err := strconv.ParseUint(string(val), 10, bits) + if err != nil { + if string(val) == "null" { + if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { + va.SetUint(0) + } + return nil + } + return newUnmarshalErrorAfterWithValue(dec, t, errors.Unwrap(err)) } + va.SetUint(n) return nil } fallthrough @@ -671,10 +693,21 @@ func makeFloatArshaler(t reflect.Type) *arshaler { if !stringify { break } - if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) && string(val) == "null" { - if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { - va.SetFloat(0) + if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) { + // For historical reasons, v1 parsed a quoted number + // according to the Go syntax and permitted a quoted null. + // See https://go.dev/issue/75619 + n, err := strconv.ParseFloat(string(val), bits) + if err != nil { + if string(val) == "null" { + if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { + va.SetFloat(0) + } + return nil + } + return newUnmarshalErrorAfterWithValue(dec, t, errors.Unwrap(err)) } + va.SetFloat(n) return nil } if n, err := jsonwire.ConsumeNumber(val); n != len(val) || err != nil { diff --git a/src/encoding/json/v2_decode_test.go b/src/encoding/json/v2_decode_test.go index f9b0a60f47..28c57ec8bf 100644 --- a/src/encoding/json/v2_decode_test.go +++ b/src/encoding/json/v2_decode_test.go @@ -1243,6 +1243,62 @@ var unmarshalTests = []struct { out: (chan int)(nil), err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[chan int]()}, }, + + // #75619 + { + CaseName: Name("QuotedInt/GoSyntax"), + in: `{"X": "-0000123"}`, + ptr: new(struct { + X int64 `json:",string"` + }), + out: struct { + X int64 `json:",string"` + }{-123}, + }, + { + CaseName: Name("QuotedInt/Invalid"), + in: `{"X": "123 "}`, + ptr: new(struct { + X int64 `json:",string"` + }), + err: &UnmarshalTypeError{Value: "number 123 ", Type: reflect.TypeFor[int64](), Field: "X", Offset: int64(len(`{"X": `))}, + }, + { + CaseName: Name("QuotedUint/GoSyntax"), + in: `{"X": "0000123"}`, + ptr: new(struct { + X uint64 `json:",string"` + }), + out: struct { + X uint64 `json:",string"` + }{123}, + }, + { + CaseName: Name("QuotedUint/Invalid"), + in: `{"X": "0x123"}`, + ptr: new(struct { + X uint64 `json:",string"` + }), + err: &UnmarshalTypeError{Value: "number 0x123", Type: reflect.TypeFor[uint64](), Field: "X", Offset: int64(len(`{"X": `))}, + }, + { + CaseName: Name("QuotedFloat/GoSyntax"), + in: `{"X": "0x1_4p-2"}`, + ptr: new(struct { + X float64 `json:",string"` + }), + out: struct { + X float64 `json:",string"` + }{0x1_4p-2}, + }, + { + CaseName: Name("QuotedFloat/Invalid"), + in: `{"X": "1.5e1_"}`, + ptr: new(struct { + X float64 `json:",string"` + }), + err: &UnmarshalTypeError{Value: "number 1.5e1_", Type: reflect.TypeFor[float64](), Field: "X", Offset: int64(len(`{"X": `))}, + }, } func TestMarshal(t *testing.T) { diff --git a/src/encoding/json/v2_options.go b/src/encoding/json/v2_options.go index 4dea88ad7e..819fe59f41 100644 --- a/src/encoding/json/v2_options.go +++ b/src/encoding/json/v2_options.go @@ -506,7 +506,9 @@ func ReportErrorsWithLegacySemantics(v bool) Options { // When marshaling, such Go values are serialized as their usual // JSON representation, but quoted within a JSON string. // When unmarshaling, such Go values must be deserialized from -// a JSON string containing their usual JSON representation. +// a JSON string containing their usual JSON representation or +// Go number representation for that numeric kind. +// Note that the Go number grammar is a superset of the JSON number grammar. // A JSON null quoted in a JSON string is a valid substitute for JSON null // while unmarshaling into a Go value that `string` takes effect on. //