encoding/json: avoid misleading errors under goexperiment.jsonv2

The jsontext package represents the location of JSON errors
using a JSON Pointer (RFC 6901). This uses the JSON type system.

Unfortunately the v1 json.UnmarshalTypeError assumes a Go struct-based
mechanism for reporting the location of errors
(and has historically never been implemented correctly since
it was a weird mix of both JSON and Go namespaces; see #43126).
Trying to map a JSON Pointer into UnmarshalTypeError.{Struct,Field}
is difficult to get right without teaching jsontext
about the Go type system.

To reduce the probability of misleading errors,
check whether the last token looks like a JSON array index
and if so, elide the phrase "into Go struct field".

Fixes #74801

Change-Id: Id2088ffb9c339a9238ed38c90223d86a89422842
Reviewed-on: https://go-review.googlesource.com/c/go/+/710676
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Joe Tsai 2025-10-11 11:57:46 -07:00 committed by Joseph Tsai
parent 11d3d2f77d
commit ee5af46172
2 changed files with 43 additions and 1 deletions

View file

@ -14,6 +14,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"encoding/json/internal/jsonwire" "encoding/json/internal/jsonwire"
"encoding/json/jsontext" "encoding/json/jsontext"
@ -119,7 +120,20 @@ type UnmarshalTypeError struct {
func (e *UnmarshalTypeError) Error() string { func (e *UnmarshalTypeError) Error() string {
var s string var s string
if e.Struct != "" || e.Field != "" { if e.Struct != "" || e.Field != "" {
s = "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() // The design of UnmarshalTypeError overly assumes a struct-based
// Go representation for the JSON value.
// The logic in jsontext represents paths using a JSON Pointer,
// which is agnostic to the Go type system.
// Trying to convert a JSON Pointer into a UnmarshalTypeError.Field
// is difficult. As a heuristic, if the last path token looks like
// an index into a JSON array (e.g., ".foo.bar.0"),
// avoid the phrase "Go struct field ".
intoWhat := "Go struct field "
i := strings.LastIndexByte(e.Field, '.') + len(".")
if len(e.Field[i:]) > 0 && strings.TrimRight(e.Field[i:], "0123456789") == "" {
intoWhat = "" // likely a Go slice or array
}
s = "json: cannot unmarshal " + e.Value + " into " + intoWhat + e.Struct + "." + e.Field + " of type " + e.Type.String()
} else { } else {
s = "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() s = "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
} }

View file

@ -2363,6 +2363,34 @@ func TestUnmarshalTypeError(t *testing.T) {
} }
} }
func TestUnmarshalTypeErrorMessage(t *testing.T) {
err := &UnmarshalTypeError{
Value: "number 5",
Type: reflect.TypeFor[int](),
Offset: 1234,
Struct: "Root",
}
for _, tt := range []struct {
field string
want string
}{
{"", "json: cannot unmarshal number 5 into Go struct field Root. of type int"},
{"1", "json: cannot unmarshal number 5 into Root.1 of type int"},
{"foo", "json: cannot unmarshal number 5 into Go struct field Root.foo of type int"},
{"foo.1", "json: cannot unmarshal number 5 into Root.foo.1 of type int"},
{"foo.bar", "json: cannot unmarshal number 5 into Go struct field Root.foo.bar of type int"},
{"foo.bar.1", "json: cannot unmarshal number 5 into Root.foo.bar.1 of type int"},
{"foo.bar.baz", "json: cannot unmarshal number 5 into Go struct field Root.foo.bar.baz of type int"},
} {
err.Field = tt.field
got := err.Error()
if got != tt.want {
t.Errorf("Error:\n\tgot: %v\n\twant: %v", got, tt.want)
}
}
}
func TestUnmarshalSyntax(t *testing.T) { func TestUnmarshalSyntax(t *testing.T) {
var x any var x any
tests := []struct { tests := []struct {