encoding/json: reduce error text regressions under goexperiment.jsonv2

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>
This commit is contained in:
Joe Tsai 2025-07-24 11:34:18 -07:00 committed by Gopher Robot
parent 0fa88dec1e
commit a6eec8bdc7
8 changed files with 76 additions and 25 deletions

View file

@ -416,6 +416,8 @@ type DoublePtr struct {
J **int J **int
} }
type NestedUnamed struct{ F struct{ V int } }
var unmarshalTests = []struct { var unmarshalTests = []struct {
CaseName CaseName
in string in string
@ -1213,6 +1215,28 @@ var unmarshalTests = []struct {
F string `json:"-,omitempty"` F string `json:"-,omitempty"`
}{"hello"}, }{"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) { func TestMarshal(t *testing.T) {

View file

@ -21,6 +21,7 @@ var AllowInternalUse NotForPublicUse
var ( var (
ErrCycle = errors.New("encountered a cycle") ErrCycle = errors.New("encountered a cycle")
ErrNonNilReference = errors.New("value must be passed as a non-nil pointer reference") 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 ( var (

View file

@ -1690,8 +1690,6 @@ func makePointerArshaler(t reflect.Type) *arshaler {
return &fncs return &fncs
} }
var errNilInterface = errors.New("cannot derive concrete type for nil interface with finite type set")
func makeInterfaceArshaler(t reflect.Type) *arshaler { func makeInterfaceArshaler(t reflect.Type) *arshaler {
// NOTE: Values retrieved from an interface are not addressable, // NOTE: Values retrieved from an interface are not addressable,
// so we shallow copy the values to make them addressable and // so we shallow copy the values to make them addressable and
@ -1797,7 +1795,7 @@ func makeInterfaceArshaler(t reflect.Type) *arshaler {
k := dec.PeekKind() k := dec.PeekKind()
if !isAnyType(t) { if !isAnyType(t) {
return newUnmarshalErrorBeforeWithSkipping(dec, uo, t, errNilInterface) return newUnmarshalErrorBeforeWithSkipping(dec, uo, t, internal.ErrNilInterface)
} }
switch k { switch k {
case 'f', 't': case 'f', 't':

View file

@ -7496,7 +7496,7 @@ func TestUnmarshal(t *testing.T) {
inBuf: `"hello"`, inBuf: `"hello"`,
inVal: new(io.Reader), inVal: new(io.Reader),
want: 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"), name: jsontest.Name("Interfaces/Empty/False"),
inBuf: `false`, inBuf: `false`,
@ -8344,7 +8344,7 @@ func TestUnmarshal(t *testing.T) {
inBuf: `{"X":"hello"}`, inBuf: `{"X":"hello"}`,
inVal: addr(struct{ X fmt.Stringer }{nil}), inVal: addr(struct{ X fmt.Stringer }{nil}),
want: 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"), name: jsontest.Name("Functions/Interface/NetIP"),
opts: []Options{ opts: []Options{

View 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. // 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 // 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). // 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 { 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, return &SemanticError{action: "unmarshal", GoType: t, Err: err,
ByteOffset: d.InputOffset() + int64(export.Decoder(d).CountNextDelimWhitespace()), 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], // newUnmarshalErrorBeforeWithSkipping is like [newUnmarshalErrorBefore],

View file

@ -117,19 +117,11 @@ type UnmarshalTypeError struct {
} }
func (e *UnmarshalTypeError) Error() string { func (e *UnmarshalTypeError) Error() string {
s := "json: cannot unmarshal" var s string
if e.Value != "" { if e.Struct != "" || e.Field != "" {
s += " JSON " + e.Value s = "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String()
} } else {
s += " into" s = "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
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()
} }
if e.Err != nil { if e.Err != nil {
s += ": " + e.Err.Error() s += ": " + e.Err.Error()

View file

@ -420,6 +420,8 @@ type DoublePtr struct {
J **int J **int
} }
type NestedUnamed struct{ F struct{ V int } }
var unmarshalTests = []struct { var unmarshalTests = []struct {
CaseName CaseName
in string in string
@ -1219,6 +1221,28 @@ var unmarshalTests = []struct {
F string `json:"-,omitempty"` F string `json:"-,omitempty"`
}{"hello"}, }{"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) { func TestMarshal(t *testing.T) {
@ -1552,12 +1576,12 @@ func TestErrorMessageFromMisusedString(t *testing.T) {
CaseName CaseName
in, err string 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":"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 JSON string into WrongString.result of Go type string: invalid character 'f' 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 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 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 JSON number into WrongString.result of Go type string`}, {Name(""), `{"result":123}`, `json: cannot unmarshal number into Go struct field WrongString.result of type string`},
{Name(""), `{"result":"\""}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: unexpected end of JSON input`}, {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 JSON string into WrongString.result of Go 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 { for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) { t.Run(tt.Name, func(t *testing.T) {
@ -2545,6 +2569,7 @@ func TestUnmarshalEmbeddedUnexported(t *testing.T) {
ptr: new(S1), ptr: new(S1),
out: &S1{R: 2}, out: &S1{R: 2},
err: &UnmarshalTypeError{ err: &UnmarshalTypeError{
Value: "number",
Type: reflect.TypeFor[S1](), Type: reflect.TypeFor[S1](),
Offset: len64(`{"R":2,"Q":`), Offset: len64(`{"R":2,"Q":`),
Struct: "S1", Struct: "S1",
@ -2577,6 +2602,7 @@ func TestUnmarshalEmbeddedUnexported(t *testing.T) {
ptr: new(S5), ptr: new(S5),
out: &S5{R: 2}, out: &S5{R: 2},
err: &UnmarshalTypeError{ err: &UnmarshalTypeError{
Value: "number",
Type: reflect.TypeFor[S5](), Type: reflect.TypeFor[S5](),
Offset: len64(`{"R":2,"Q":`), Offset: len64(`{"R":2,"Q":`),
Struct: "S5", Struct: "S5",

View file

@ -73,6 +73,9 @@ func transformUnmarshalError(root any, err error) error {
if err.Err == jsonv2.ErrUnknownName { if err.Err == jsonv2.ErrUnknownName {
return fmt.Errorf("json: unknown field %q", err.JSONPointer.LastToken()) 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 // Historically, UnmarshalTypeError has always been inconsistent
// about how it reported position information. // about how it reported position information.