diff options
| author | Joe Tsai <joetsai@digital-static.net> | 2025-08-12 01:56:43 -0700 |
|---|---|---|
| committer | Joseph Tsai <joetsai@digital-static.net> | 2025-08-13 15:47:31 -0700 |
| commit | af8870708bbaf15956a27cbab15582b4c666855e (patch) | |
| tree | f60725fc5fcc77431142aff96c04fbe53f50c1a8 /src/encoding | |
| parent | 0a75e5a07b858cbe6216c99fa12d582d063499d9 (diff) | |
| download | go-af8870708bbaf15956a27cbab15582b4c666855e.tar.xz | |
encoding/json/v2: fix incorrect marshaling of NaN in float64 any
There is a fast-path optimization for marshaling an any type
that should be semantically identical to when the optimization
is not active (i.e., optimizeCommon is false).
Unfortunately, the optimization accidentally allows NaN,
which this change fixes.
The source of this discrepency is that Encoder.WriteToken(Float(math.NaN()))
emits a JSON string with "NaN", rather than report an error.
The rationale for this behavior is because we needed to decide what to do
with Float(math.NaN()), whether it would return an error, panic, or allow it.
To keep the API simpler (no errors) and less sharp (no panics), we permitted NaN.
The fact that WriteToken allowed it is a logical extension of that decision,
but we could decide to disallow it at least within WriteToken.
As things stand, it is already inconsistent between json/v2 and jsontext, where
json/v2 rejects NaN by default in Marshal, but jsontext allows it in WriteToken.
This only modifies code that is compiled under goexperiment.jsonv2.
Fixes #74797
Change-Id: Ib0708cfbf93c2b059c0a85e4c4544c0604573448
Reviewed-on: https://go-review.googlesource.com/c/go/+/695276
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Diffstat (limited to 'src/encoding')
| -rw-r--r-- | src/encoding/json/v2/arshal_any.go | 20 | ||||
| -rw-r--r-- | src/encoding/json/v2/arshal_test.go | 5 |
2 files changed, 17 insertions, 8 deletions
diff --git a/src/encoding/json/v2/arshal_any.go b/src/encoding/json/v2/arshal_any.go index c2b09bd918..97a77e9237 100644 --- a/src/encoding/json/v2/arshal_any.go +++ b/src/encoding/json/v2/arshal_any.go @@ -8,6 +8,7 @@ package json import ( "cmp" + "math" "reflect" "strconv" @@ -35,20 +36,23 @@ func marshalValueAny(enc *jsontext.Encoder, val any, mo *jsonopts.Struct) error case string: return enc.WriteToken(jsontext.String(val)) case float64: + if math.IsNaN(val) || math.IsInf(val, 0) { + break // use default logic below + } return enc.WriteToken(jsontext.Float(val)) case map[string]any: return marshalObjectAny(enc, val, mo) case []any: return marshalArrayAny(enc, val, mo) - default: - v := newAddressableValue(reflect.TypeOf(val)) - v.Set(reflect.ValueOf(val)) - marshal := lookupArshaler(v.Type()).marshal - if mo.Marshalers != nil { - marshal, _ = mo.Marshalers.(*Marshalers).lookup(marshal, v.Type()) - } - return marshal(enc, v, mo) } + + v := newAddressableValue(reflect.TypeOf(val)) + v.Set(reflect.ValueOf(val)) + marshal := lookupArshaler(v.Type()).marshal + if mo.Marshalers != nil { + marshal, _ = mo.Marshalers.(*Marshalers).lookup(marshal, v.Type()) + } + return marshal(enc, v, mo) } // unmarshalValueAny unmarshals a JSON value as a Go any. diff --git a/src/encoding/json/v2/arshal_test.go b/src/encoding/json/v2/arshal_test.go index 5f5f072e25..75093345a3 100644 --- a/src/encoding/json/v2/arshal_test.go +++ b/src/encoding/json/v2/arshal_test.go @@ -3217,6 +3217,11 @@ func TestMarshal(t *testing.T) { in: struct{ X any }{[8]byte{}}, want: `{"X":"called"}`, }, { + name: jsontest.Name("Interfaces/Any/Float/NaN"), + in: struct{ X any }{math.NaN()}, + want: `{"X"`, + wantErr: EM(fmt.Errorf("unsupported value: %v", math.NaN())).withType(0, reflect.TypeFor[float64]()).withPos(`{"X":`, "/X"), + }, { name: jsontest.Name("Interfaces/Any/Maps/Nil"), in: struct{ X any }{map[string]any(nil)}, want: `{"X":{}}`, |
