]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: reduce error text regressions under goexperiment.jsonv2
authorJoe Tsai <joetsai@digital-static.net>
Thu, 24 Jul 2025 18:34:18 +0000 (11:34 -0700)
committerGopher Robot <gobot@golang.org>
Fri, 25 Jul 2025 03:30:25 +0000 (20:30 -0700)
There were minor and unnecessary error text changes
when v1 was implemented using v2.
Reduce divergences if possible.

Of the cases reported in #74713, there are no more differences for:

v1: json: cannot unmarshal number into Go value of type chan int
v2: json: cannot unmarshal number into Go value of type chan int

and

v1: json: cannot unmarshal number into Go value of type error
v2: json: cannot unmarshal number into Go value of type error

However, there is a difference between:

v1: json: cannot unmarshal string into Go struct field .F.V of type int
v2: json: cannot unmarshal string into Go struct field S.F.V of type int

For reasons unclear, the v1 logic was always inconsistent about
whether it could properly record the root struct type,
while the v1 emulation layer under v2 is always able to.

This only modifies code that is compiled in under goexperiment.jsonv2.

Fixes #74713

Change-Id: I9e87323b1810130cb929288fdd86aff4be82d5f2
Reviewed-on: https://go-review.googlesource.com/c/go/+/689918
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 Knyszek <mknyszek@google.com>
src/encoding/json/decode_test.go
src/encoding/json/internal/internal.go
src/encoding/json/v2/arshal_default.go
src/encoding/json/v2/arshal_test.go
src/encoding/json/v2/errors.go
src/encoding/json/v2_decode.go
src/encoding/json/v2_decode_test.go
src/encoding/json/v2_inject.go

index 0df31c82c821e76d99bc78baca74918b84bd9618..d12495f90b7141123f1db90296538d6fe35bfcab 100644 (file)
@@ -416,6 +416,8 @@ type DoublePtr struct {
        J **int
 }
 
+type NestedUnamed struct{ F struct{ V int } }
+
 var unmarshalTests = []struct {
        CaseName
        in                    string
@@ -1213,6 +1215,28 @@ var unmarshalTests = []struct {
                        F string `json:"-,omitempty"`
                }{"hello"},
        },
+
+       {
+               CaseName: Name("ErrorForNestedUnamed"),
+               in:       `{"F":{"V":"s"}}`,
+               ptr:      new(NestedUnamed),
+               out:      NestedUnamed{},
+               err:      &UnmarshalTypeError{Value: "string", Type: reflect.TypeFor[int](), Offset: 13, Field: "F.V"},
+       },
+       {
+               CaseName: Name("ErrorInterface"),
+               in:       `1`,
+               ptr:      new(error),
+               out:      error(nil),
+               err:      &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[error](), Offset: 1},
+       },
+       {
+               CaseName: Name("ErrorChan"),
+               in:       `1`,
+               ptr:      new(chan int),
+               out:      (chan int)(nil),
+               err:      &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[chan int](), Offset: 1},
+       },
 }
 
 func TestMarshal(t *testing.T) {
index f587c7b32c964716c2e51a6a998229332a8458e5..c95f83fe44091322e6d9b47a77752481c984cbc2 100644 (file)
@@ -21,6 +21,7 @@ var AllowInternalUse NotForPublicUse
 var (
        ErrCycle           = errors.New("encountered a cycle")
        ErrNonNilReference = errors.New("value must be passed as a non-nil pointer reference")
+       ErrNilInterface    = errors.New("cannot derive concrete type for nil interface with finite type set")
 )
 
 var (
index 0b30ac4fb7d600ddb21863159a8c8faffa816852..f3fc79beac0af4cc546223d92997904a1c2cd76b 100644 (file)
@@ -1690,8 +1690,6 @@ func makePointerArshaler(t reflect.Type) *arshaler {
        return &fncs
 }
 
-var errNilInterface = errors.New("cannot derive concrete type for nil interface with finite type set")
-
 func makeInterfaceArshaler(t reflect.Type) *arshaler {
        // NOTE: Values retrieved from an interface are not addressable,
        // so we shallow copy the values to make them addressable and
@@ -1797,7 +1795,7 @@ func makeInterfaceArshaler(t reflect.Type) *arshaler {
 
                        k := dec.PeekKind()
                        if !isAnyType(t) {
-                               return newUnmarshalErrorBeforeWithSkipping(dec, uo, t, errNilInterface)
+                               return newUnmarshalErrorBeforeWithSkipping(dec, uo, t, internal.ErrNilInterface)
                        }
                        switch k {
                        case 'f', 't':
index 88887e1b0086a85eb43f49ef9f26ef62d28ef387..f1ee2e2e3a73655ea06a9ed01f40fc80841810ab 100644 (file)
@@ -7496,7 +7496,7 @@ func TestUnmarshal(t *testing.T) {
                inBuf:   `"hello"`,
                inVal:   new(io.Reader),
                want:    new(io.Reader),
-               wantErr: EU(errNilInterface).withType(0, T[io.Reader]()),
+               wantErr: EU(internal.ErrNilInterface).withType(0, T[io.Reader]()),
        }, {
                name:  jsontest.Name("Interfaces/Empty/False"),
                inBuf: `false`,
@@ -8344,7 +8344,7 @@ func TestUnmarshal(t *testing.T) {
                inBuf:   `{"X":"hello"}`,
                inVal:   addr(struct{ X fmt.Stringer }{nil}),
                want:    addr(struct{ X fmt.Stringer }{nil}),
-               wantErr: EU(errNilInterface).withPos(`{"X":`, "/X").withType(0, T[fmt.Stringer]()),
+               wantErr: EU(internal.ErrNilInterface).withPos(`{"X":`, "/X").withType(0, T[fmt.Stringer]()),
        }, {
                name: jsontest.Name("Functions/Interface/NetIP"),
                opts: []Options{
index 48cdcc953b5f711add6bb1e58a6ca3cdb296c8a6..1f31505869238159b9ae936df634a0806e242e71 100644 (file)
@@ -120,10 +120,17 @@ func newMarshalErrorBefore(e *jsontext.Encoder, t reflect.Type, err error) error
 // is positioned right before the next token or value, which causes an error.
 // It does not record the next JSON kind as this error is used to indicate
 // the receiving Go value is invalid to unmarshal into (and not a JSON error).
+// However, if [jsonflags.ReportErrorsWithLegacySemantics] is specified,
+// then it does record the next JSON kind for historical reporting reasons.
 func newUnmarshalErrorBefore(d *jsontext.Decoder, t reflect.Type, err error) error {
+       var k jsontext.Kind
+       if export.Decoder(d).Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) {
+               k = d.PeekKind()
+       }
        return &SemanticError{action: "unmarshal", GoType: t, Err: err,
                ByteOffset:  d.InputOffset() + int64(export.Decoder(d).CountNextDelimWhitespace()),
-               JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, +1))}
+               JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, +1)),
+               JSONKind:    k}
 }
 
 // newUnmarshalErrorBeforeWithSkipping is like [newUnmarshalErrorBefore],
index c82ee903c33c112f0e972d91560742e1e0f54f15..1041ec7ee402d4c0d073338d80cd9b4d13806c21 100644 (file)
@@ -117,19 +117,11 @@ type UnmarshalTypeError struct {
 }
 
 func (e *UnmarshalTypeError) Error() string {
-       s := "json: cannot unmarshal"
-       if e.Value != "" {
-               s += " JSON " + e.Value
-       }
-       s += " into"
-       var preposition string
-       if e.Field != "" {
-               s += " " + e.Struct + "." + e.Field
-               preposition = " of"
-       }
-       if e.Type != nil {
-               s += preposition
-               s += " Go type " + e.Type.String()
+       var s string
+       if e.Struct != "" || e.Field != "" {
+               s = "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String()
+       } else {
+               s = "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
        }
        if e.Err != nil {
                s += ": " + e.Err.Error()
index 1e4914efc4aeab7ef2243d87a1501429718ec287..f9b0a60f47cfd5732f2d072fcf06ef4be84e93fc 100644 (file)
@@ -420,6 +420,8 @@ type DoublePtr struct {
        J **int
 }
 
+type NestedUnamed struct{ F struct{ V int } }
+
 var unmarshalTests = []struct {
        CaseName
        in                    string
@@ -1219,6 +1221,28 @@ var unmarshalTests = []struct {
                        F string `json:"-,omitempty"`
                }{"hello"},
        },
+
+       {
+               CaseName: Name("ErrorForNestedUnamed"),
+               in:       `{"F":{"V":"s"}}`,
+               ptr:      new(NestedUnamed),
+               out:      NestedUnamed{},
+               err:      &UnmarshalTypeError{Value: "string", Type: reflect.TypeFor[int](), Offset: 10, Struct: "NestedUnamed", Field: "F.V"},
+       },
+       {
+               CaseName: Name("ErrorInterface"),
+               in:       `1`,
+               ptr:      new(error),
+               out:      error(nil),
+               err:      &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[error]()},
+       },
+       {
+               CaseName: Name("ErrorChan"),
+               in:       `1`,
+               ptr:      new(chan int),
+               out:      (chan int)(nil),
+               err:      &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[chan int]()},
+       },
 }
 
 func TestMarshal(t *testing.T) {
@@ -1552,12 +1576,12 @@ func TestErrorMessageFromMisusedString(t *testing.T) {
                CaseName
                in, err string
        }{
-               {Name(""), `{"result":"x"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: invalid character 'x' looking for beginning of object key string`},
-               {Name(""), `{"result":"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: invalid character 'f' looking for beginning of object key string`},
-               {Name(""), `{"result":"123"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: invalid character '1' looking for beginning of object key string`},
-               {Name(""), `{"result":123}`, `json: cannot unmarshal JSON number into WrongString.result of Go type string`},
-               {Name(""), `{"result":"\""}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: unexpected end of JSON input`},
-               {Name(""), `{"result":"\"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: unexpected end of JSON input`},
+               {Name(""), `{"result":"x"}`, `json: cannot unmarshal string into Go struct field WrongString.result of type string: invalid character 'x' looking for beginning of object key string`},
+               {Name(""), `{"result":"foo"}`, `json: cannot unmarshal string into Go struct field WrongString.result of type string: invalid character 'f' looking for beginning of object key string`},
+               {Name(""), `{"result":"123"}`, `json: cannot unmarshal string into Go struct field WrongString.result of type string: invalid character '1' looking for beginning of object key string`},
+               {Name(""), `{"result":123}`, `json: cannot unmarshal number into Go struct field WrongString.result of type string`},
+               {Name(""), `{"result":"\""}`, `json: cannot unmarshal string into Go struct field WrongString.result of type string: unexpected end of JSON input`},
+               {Name(""), `{"result":"\"foo"}`, `json: cannot unmarshal string into Go struct field WrongString.result of type string: unexpected end of JSON input`},
        }
        for _, tt := range tests {
                t.Run(tt.Name, func(t *testing.T) {
@@ -2545,6 +2569,7 @@ func TestUnmarshalEmbeddedUnexported(t *testing.T) {
                ptr:      new(S1),
                out:      &S1{R: 2},
                err: &UnmarshalTypeError{
+                       Value:  "number",
                        Type:   reflect.TypeFor[S1](),
                        Offset: len64(`{"R":2,"Q":`),
                        Struct: "S1",
@@ -2577,6 +2602,7 @@ func TestUnmarshalEmbeddedUnexported(t *testing.T) {
                ptr:      new(S5),
                out:      &S5{R: 2},
                err: &UnmarshalTypeError{
+                       Value:  "number",
                        Type:   reflect.TypeFor[S5](),
                        Offset: len64(`{"R":2,"Q":`),
                        Struct: "S5",
index f903588431b4d858444ac69ac7b67c7b6711a2d6..31cdb4d61a9488b99dceece5bbb2ab12d695b475 100644 (file)
@@ -73,6 +73,9 @@ func transformUnmarshalError(root any, err error) error {
                if err.Err == jsonv2.ErrUnknownName {
                        return fmt.Errorf("json: unknown field %q", err.JSONPointer.LastToken())
                }
+               if err.Err == internal.ErrNilInterface {
+                       err.Err = nil // non-descriptive for historical reasons
+               }
 
                // Historically, UnmarshalTypeError has always been inconsistent
                // about how it reported position information.