From e27260cd2b2e9e553c9ccbf563fa4a63a9898c1d Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 19 Nov 2024 16:56:58 -0800 Subject: [PATCH] Reject JSON numbers with StringifyNumbers when unmarshaling (#60) WARNING: This commit includes breaking changes. Previously, if StringifyNumbers was specified, we would permit unmarshaling of a JSON number into a Go number. However, this is not how the v1 equivalent functionality operated. In v1, once the `string` tag option was specified, it would only unmarshal a JSON string containing a JSON number, but would reject a JSON number. It may be reasonable to provide a separate option that allows the more flexible unmarshal behavior that this commit breaks, but that can be a future change. For now, we are working towards implementing v1 entirely in terms of v2. --- arshal_default.go | 9 +++++++++ arshal_test.go | 47 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/arshal_default.go b/arshal_default.go index 88d22bf..d491500 100644 --- a/arshal_default.go +++ b/arshal_default.go @@ -409,6 +409,9 @@ func makeIntArshaler(t reflect.Type) *arshaler { val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) fallthrough case '0': + if uo.Flags.Get(jsonflags.StringifyNumbers) && k == '0' { + break + } var negOffset int neg := len(val) > 0 && val[0] == '-' if neg { @@ -486,6 +489,9 @@ func makeUintArshaler(t reflect.Type) *arshaler { val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) fallthrough case '0': + if uo.Flags.Get(jsonflags.StringifyNumbers) && k == '0' { + break + } n, ok := jsonwire.ParseUint(val) maxUint := uint64(1) << bits overflow := n > maxUint-1 @@ -590,6 +596,9 @@ func makeFloatArshaler(t reflect.Type) *arshaler { } fallthrough case '0': + if uo.Flags.Get(jsonflags.StringifyNumbers) && k == '0' { + break + } fv, ok := jsonwire.ParseFloat(val, bits) if !ok && uo.Flags.Get(jsonflags.RejectFloatOverflow) { return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: strconv.ErrRange} diff --git a/arshal_test.go b/arshal_test.go index 2e11403..3b36d92 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -4685,6 +4685,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `"-6464"`, inVal: new(int), want: addr(int(-6464)), + }, { + name: jsontest.Name("Ints/Stringified/Invalid"), + opts: []Options{StringifyNumbers(true)}, + inBuf: `-6464`, + inVal: new(int), + want: new(int), + wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: intType}, }, { name: jsontest.Name("Ints/Stringified/LeadingZero"), opts: []Options{StringifyNumbers(true)}, @@ -4869,6 +4876,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `"6464"`, inVal: new(uint), want: addr(uint(6464)), + }, { + name: jsontest.Name("Uints/Stringified/Invalid"), + opts: []Options{StringifyNumbers(true)}, + inBuf: `6464`, + inVal: new(uint), + want: new(uint), + wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uintType}, }, { name: jsontest.Name("Uints/Stringified/LeadingZero"), opts: []Options{StringifyNumbers(true)}, @@ -5042,6 +5056,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `"64.64"`, inVal: new(float64), want: addr(float64(64.64)), + }, { + name: jsontest.Name("Floats/Stringified/Invalid"), + opts: []Options{StringifyNumbers(true)}, + inBuf: `64.64`, + inVal: new(float64), + want: new(float64), + wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: float64Type}, }, { name: jsontest.Name("Floats/Escaped"), opts: []Options{StringifyNumbers(true)}, @@ -5488,33 +5509,33 @@ func TestUnmarshal(t *testing.T) { "Bool": true, "String": "hello", "Bytes": "AQID", - "Int": -64, - "Uint": 64, - "Float": 3.14159, + "Int": "-64", + "Uint": "64", + "Float": "3.14159", "Map": {"key": "value"}, "StructScalars": { "Bool": true, "String": "hello", "Bytes": "AQID", - "Int": -64, - "Uint": 64, - "Float": 3.14159 + "Int": "-64", + "Uint": "64", + "Float": "3.14159" }, "StructMaps": { "MapBool": {"": true}, "MapString": {"": "hello"}, "MapBytes": {"": "AQID"}, - "MapInt": {"": -64}, - "MapUint": {"": 64}, - "MapFloat": {"": 3.14159} + "MapInt": {"": "-64"}, + "MapUint": {"": "64"}, + "MapFloat": {"": "3.14159"} }, "StructSlices": { "SliceBool": [true], "SliceString": ["hello"], "SliceBytes": ["AQID"], - "SliceInt": [-64], - "SliceUint": [64], - "SliceFloat": [3.14159] + "SliceInt": ["-64"], + "SliceUint": ["64"], + "SliceFloat": ["3.14159"] }, "Slice": ["fizz","buzz"], "Array": ["goodbye"], @@ -6318,7 +6339,7 @@ func TestUnmarshal(t *testing.T) { }, { name: jsontest.Name("Structs/InlinedFallback/MapStringInt/StringifiedNumbers"), opts: []Options{StringifyNumbers(true)}, - inBuf: `{"zero": 0, "one": "1", "two": 2}`, + inBuf: `{"zero": "0", "one": "1", "two": "2"}`, inVal: new(structInlineMapStringInt), want: addr(structInlineMapStringInt{ X: map[string]int{"zero": 0, "one": 1, "two": 2},