Skip to content

Commit

Permalink
encoding/json: return JSON field with all marshal errors
Browse files Browse the repository at this point in the history
  • Loading branch information
thequailman committed Nov 16, 2024
1 parent 252e9de commit f2091a5
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 8 deletions.
61 changes: 57 additions & 4 deletions src/encoding/json/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,24 @@ type UnmarshalTypeError struct {
Offset int64 // error occurred after reading Offset bytes
Struct string // name of the struct type containing the field
Field string // the full path from root node to the field, include embedded struct
Err error // Error which occurred during unmarshal
}

func (e *UnmarshalTypeError) Error() string {
if e.Err != nil {
return e.Err.Error()
}

if e.Struct != "" || e.Field != "" {
return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String()
}
return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
}

func (e *UnmarshalTypeError) Unwrap() error {
return e.Err
}

// An UnmarshalFieldError describes a JSON object key that
// led to an unexported (and therefore unwritable) struct field.
//
Expand Down Expand Up @@ -508,7 +517,15 @@ func (d *decodeState) array(v reflect.Value) error {
if u != nil {
start := d.readIndex()
d.skip()
return u.UnmarshalJSON(d.data[start:d.off])
if err := u.UnmarshalJSON(d.data[start:d.off]); err != nil {
if e, ok := err.(*UnmarshalTypeError); ok {
return e
}

d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off), Err: err})
}

return nil
}
if ut != nil {
d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)})
Expand Down Expand Up @@ -605,7 +622,16 @@ func (d *decodeState) object(v reflect.Value) error {
if u != nil {
start := d.readIndex()
d.skip()
return u.UnmarshalJSON(d.data[start:d.off])
err := u.UnmarshalJSON(d.data[start:d.off])
if err != nil {
if e, ok := err.(*UnmarshalTypeError); ok {
return e
}

d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off), Err: err})
}

return nil
}
if ut != nil {
d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)})
Expand Down Expand Up @@ -861,7 +887,28 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool
isNull := item[0] == 'n' // null
u, ut, pv := indirect(v, isNull)
if u != nil {
return u.UnmarshalJSON(item)
err := u.UnmarshalJSON(item)
if err != nil {
if e, ok := err.(*UnmarshalTypeError); ok {
return e
} else {
var val string
switch item[0] {
case '"':
val = "string"
case 'n':
val = "null"
case 't', 'f':
val = "bool"
default:
val = "number"
}

d.saveError(&UnmarshalTypeError{Value: val, Type: v.Type(), Offset: int64(d.readIndex()), Err: err})
}
}

return nil
}
if ut != nil {
if item[0] != '"' {
Expand All @@ -886,7 +933,13 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool
}
panic(phasePanicMsg)
}
return ut.UnmarshalText(s)

err := ut.UnmarshalText(s)
if err != nil {
d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex()), Err: err})
}

return nil
}

v = pv
Expand Down
21 changes: 17 additions & 4 deletions src/encoding/json/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ type TOuter struct {
T TAlias
}

type EV struct {
EVS EVS
}

type EVS string

var EVerr = errors.New("error")

func (*EVS) UnmarshalJSON(_ []byte) error {
return EVerr
}

// ifaceNumAsFloat64/ifaceNumAsNumber are used to test unmarshaling with and
// without UseNumber
var ifaceNumAsFloat64 = map[string]any{
Expand Down Expand Up @@ -437,17 +449,18 @@ var unmarshalTests = []struct {
{CaseName: Name(""), in: `"g-clef: \uD834\uDD1E"`, ptr: new(string), out: "g-clef: \U0001D11E"},
{CaseName: Name(""), in: `"invalid: \uD834x\uDD1E"`, ptr: new(string), out: "invalid: \uFFFDx\uFFFD"},
{CaseName: Name(""), in: "null", ptr: new(any), out: nil},
{CaseName: Name(""), in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeFor[string](), 7, "T", "X"}},
{CaseName: Name(""), in: `{"X": 23}`, ptr: new(T), out: T{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), 8, "T", "X"}},
{CaseName: Name(""), in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeFor[string](), 7, "T", "X", nil}},
{CaseName: Name(""), in: `{"X": 23}`, ptr: new(T), out: T{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), 8, "T", "X", nil}},
{CaseName: Name(""), in: `{"x": 1}`, ptr: new(tx), out: tx{}},
{CaseName: Name(""), in: `{"x": 1}`, ptr: new(tx), out: tx{}},
{CaseName: Name(""), in: `{"x": 1}`, ptr: new(tx), err: fmt.Errorf("json: unknown field \"x\""), disallowUnknownFields: true},
{CaseName: Name(""), in: `{"S": 23}`, ptr: new(W), out: W{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[SS](), 0, "W", "S"}},
{CaseName: Name(""), in: `{"T": {"X": 23}}`, ptr: new(TOuter), out: TOuter{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), 0, "TOuter", "T.X"}},
{CaseName: Name(""), in: `{"S": 23}`, ptr: new(W), out: W{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[SS](), 0, "W", "S", nil}},
{CaseName: Name(""), in: `{"T": {"X": 23}}`, ptr: new(TOuter), out: TOuter{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), 0, "TOuter", "T.X", nil}},
{CaseName: Name(""), in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: Number("3")}},
{CaseName: Name(""), in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: Number("1"), F2: int32(2), F3: Number("3")}, useNumber: true},
{CaseName: Name(""), in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(any), out: ifaceNumAsFloat64},
{CaseName: Name(""), in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(any), out: ifaceNumAsNumber, useNumber: true},
{CaseName: Name(""), in: `{"EVS": 23}`, ptr: new(EV), out: EV{}, err: &UnmarshalTypeError{"string", reflect.TypeFor[EVS](), 0, "EV", "EVS", EVerr}},

// raw values with whitespace
{CaseName: Name(""), in: "\n true ", ptr: new(bool), out: true},
Expand Down

0 comments on commit f2091a5

Please sign in to comment.