]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json/v2: fix incorrect marshaling of NaN in float64 any
authorJoe Tsai <joetsai@digital-static.net>
Tue, 12 Aug 2025 08:56:43 +0000 (01:56 -0700)
committerJoseph Tsai <joetsai@digital-static.net>
Wed, 13 Aug 2025 22:47:31 +0000 (15:47 -0700)
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>

src/encoding/json/v2/arshal_any.go
src/encoding/json/v2/arshal_test.go

index c2b09bd918fae23bf398d79ec772a83e6db10c8f..97a77e923766d9c691ef4c4a2677e21490694c3a 100644 (file)
@@ -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.
index 5f5f072e25042993cedc289d8e0bc6c8a38abf25..75093345a3b93eb6f6a41cfec64750bf78d3c74d 100644 (file)
@@ -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)},