diff --git a/src/encoding/json/decode_test.go b/src/encoding/json/decode_test.go index 0df31c82c82..d12495f90b7 100644 --- a/src/encoding/json/decode_test.go +++ b/src/encoding/json/decode_test.go @@ -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) { diff --git a/src/encoding/json/internal/internal.go b/src/encoding/json/internal/internal.go index f587c7b32c9..c95f83fe440 100644 --- a/src/encoding/json/internal/internal.go +++ b/src/encoding/json/internal/internal.go @@ -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 ( diff --git a/src/encoding/json/v2/arshal_default.go b/src/encoding/json/v2/arshal_default.go index 0b30ac4fb7d..f3fc79beac0 100644 --- a/src/encoding/json/v2/arshal_default.go +++ b/src/encoding/json/v2/arshal_default.go @@ -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': diff --git a/src/encoding/json/v2/arshal_test.go b/src/encoding/json/v2/arshal_test.go index 88887e1b008..f1ee2e2e3a7 100644 --- a/src/encoding/json/v2/arshal_test.go +++ b/src/encoding/json/v2/arshal_test.go @@ -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{ diff --git a/src/encoding/json/v2/errors.go b/src/encoding/json/v2/errors.go index 48cdcc953b5..1f315058692 100644 --- a/src/encoding/json/v2/errors.go +++ b/src/encoding/json/v2/errors.go @@ -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], diff --git a/src/encoding/json/v2_decode.go b/src/encoding/json/v2_decode.go index c82ee903c33..1041ec7ee40 100644 --- a/src/encoding/json/v2_decode.go +++ b/src/encoding/json/v2_decode.go @@ -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() diff --git a/src/encoding/json/v2_decode_test.go b/src/encoding/json/v2_decode_test.go index 1e4914efc4a..f9b0a60f47c 100644 --- a/src/encoding/json/v2_decode_test.go +++ b/src/encoding/json/v2_decode_test.go @@ -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", diff --git a/src/encoding/json/v2_inject.go b/src/encoding/json/v2_inject.go index f903588431b..31cdb4d61a9 100644 --- a/src/encoding/json/v2_inject.go +++ b/src/encoding/json/v2_inject.go @@ -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.