From: Joe Tsai Date: Tue, 12 Aug 2025 08:56:43 +0000 (-0700) Subject: encoding/json/v2: fix incorrect marshaling of NaN in float64 any X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=af8870708bbaf15956a27cbab15582b4c666855e;p=gostls13.git 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 Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI --- 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 @@ -3216,6 +3216,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)},