]> Cypherpunks repositories - gostls13.git/commitdiff
encoding/json: avoid misleading errors under goexperiment.jsonv2
authorJoe Tsai <joetsai@digital-static.net>
Sat, 11 Oct 2025 18:57:46 +0000 (11:57 -0700)
committerJoseph Tsai <joetsai@digital-static.net>
Tue, 14 Oct 2025 19:20:23 +0000 (12:20 -0700)
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>
src/encoding/json/v2_decode.go
src/encoding/json/v2_decode_test.go

index 1041ec7ee402d4c0d073338d80cd9b4d13806c21..f17d7ebccada0abb539d6e6d24648234b2383fd8 100644 (file)
@@ -14,6 +14,7 @@ import (
        "fmt"
        "reflect"
        "strconv"
+       "strings"
 
        "encoding/json/internal/jsonwire"
        "encoding/json/jsontext"
@@ -119,7 +120,20 @@ type UnmarshalTypeError struct {
 func (e *UnmarshalTypeError) Error() 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()
+               // 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 {
                s = "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
        }
index 28c57ec8bf5c736cd9489be9e3c721603aa173d9..26b4448721e4ece17d7689616c46b77ff31d5064 100644 (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) {
        var x any
        tests := []struct {