]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: fix regression in quoted numbers under goexperiment.jsonv2
authorJoe Tsai <joetsai@digital-static.net>
Mon, 6 Oct 2025 19:56:29 +0000 (12:56 -0700)
committerJoseph Tsai <joetsai@digital-static.net>
Sat, 11 Oct 2025 01:03:36 +0000 (18:03 -0700)
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 <dneil@google.com>
Auto-Submit: Joseph Tsai <joetsai@digital-static.net>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
src/encoding/json/decode_test.go
src/encoding/json/v2/arshal_default.go
src/encoding/json/v2_decode_test.go
src/encoding/json/v2_options.go

index d12495f90b7141123f1db90296538d6fe35bfcab..0b26b8eb91813ba21f726abfd7a89168425ddfec 100644 (file)
@@ -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) {
index c2307fa31d7fcc3d71f8fe9b09a4664fcebc75a6..078d345e1439c2ab51df0802aca7ebfbea3f1a6c 100644 (file)
@@ -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 {
index f9b0a60f47cfd5732f2d072fcf06ef4be84e93fc..28c57ec8bf5c736cd9489be9e3c721603aa173d9 100644 (file)
@@ -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) {
index 4dea88ad7edaf60bd33591d00ec4287cb0219f0c..819fe59f412c683be5d6d411e306e8695465f57e 100644 (file)
@@ -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.
 //