Skip to content

Commit be5ad23

Browse files
committed
Output field path in strict errors
1 parent c049b76 commit be5ad23

File tree

5 files changed

+106
-19
lines changed

5 files changed

+106
-19
lines changed

internal/golang/encoding/json/decode.go

+40-5
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ type decodeState struct {
234234

235235
savedStrictErrors []error
236236
seenStrictErrors map[string]struct{}
237+
strictFieldStack []string
237238

238239
caseSensitive bool
239240

@@ -261,6 +262,8 @@ func (d *decodeState) init(data []byte) *decodeState {
261262
// Reuse the allocated space for the FieldStack slice.
262263
d.errorContext.FieldStack = d.errorContext.FieldStack[:0]
263264
}
265+
// Reuse the allocated space for the strict FieldStack slice.
266+
d.strictFieldStack = d.strictFieldStack[:0]
264267
return d
265268
}
266269

@@ -555,6 +558,12 @@ func (d *decodeState) array(v reflect.Value) error {
555558
break
556559
}
557560

561+
origStrictFieldStackLen := len(d.strictFieldStack)
562+
defer func() {
563+
// Reset to original length and reuse the allocated space for the strict FieldStack slice.
564+
d.strictFieldStack = d.strictFieldStack[:origStrictFieldStackLen]
565+
}()
566+
558567
i := 0
559568
for {
560569
// Look ahead for ] - can only happen on first iteration.
@@ -580,6 +589,7 @@ func (d *decodeState) array(v reflect.Value) error {
580589
}
581590
}
582591

592+
d.appendStrictFieldStackIndex(i)
583593
if i < v.Len() {
584594
// Decode into element.
585595
if err := d.value(v.Index(i)); err != nil {
@@ -591,6 +601,8 @@ func (d *decodeState) array(v reflect.Value) error {
591601
return err
592602
}
593603
}
604+
// Reset to original length and reuse the allocated space for the strict FieldStack slice.
605+
d.strictFieldStack = d.strictFieldStack[:origStrictFieldStackLen]
594606
i++
595607

596608
// Next token must be , or ].
@@ -683,7 +695,7 @@ func (d *decodeState) object(v reflect.Value) error {
683695
seenKeys = map[string]struct{}{}
684696
}
685697
if _, seen := seenKeys[fieldName]; seen {
686-
d.saveStrictError(fmt.Errorf("duplicate field %q", fieldName))
698+
d.saveStrictError(d.newFieldError("duplicate field", fieldName))
687699
} else {
688700
seenKeys[fieldName] = struct{}{}
689701
}
@@ -699,7 +711,7 @@ func (d *decodeState) object(v reflect.Value) error {
699711
var seenKeys uint64
700712
checkDuplicateField = func(fieldNameIndex int, fieldName string) {
701713
if seenKeys&(1<<fieldNameIndex) != 0 {
702-
d.saveStrictError(fmt.Errorf("duplicate field %q", fieldName))
714+
d.saveStrictError(d.newFieldError("duplicate field", fieldName))
703715
} else {
704716
seenKeys = seenKeys | (1 << fieldNameIndex)
705717
}
@@ -712,7 +724,7 @@ func (d *decodeState) object(v reflect.Value) error {
712724
seenIndexes = make([]bool, len(fields.list))
713725
}
714726
if seenIndexes[fieldNameIndex] {
715-
d.saveStrictError(fmt.Errorf("duplicate field %q", fieldName))
727+
d.saveStrictError(d.newFieldError("duplicate field", fieldName))
716728
} else {
717729
seenIndexes[fieldNameIndex] = true
718730
}
@@ -732,6 +744,7 @@ func (d *decodeState) object(v reflect.Value) error {
732744
if d.errorContext != nil {
733745
origErrorContext = *d.errorContext
734746
}
747+
origStrictFieldStackLen := len(d.strictFieldStack)
735748

736749
for {
737750
// Read opening " of string key or closing }.
@@ -768,6 +781,7 @@ func (d *decodeState) object(v reflect.Value) error {
768781
if checkDuplicateField != nil {
769782
checkDuplicateField(0, string(key))
770783
}
784+
d.appendStrictFieldStackKey(string(key))
771785
} else {
772786
var f *field
773787
if i, ok := fields.nameIndex[string(key)]; ok {
@@ -820,8 +834,9 @@ func (d *decodeState) object(v reflect.Value) error {
820834
}
821835
d.errorContext.FieldStack = append(d.errorContext.FieldStack, f.name)
822836
d.errorContext.Struct = t
837+
d.appendStrictFieldStackKey(f.name)
823838
} else if d.disallowUnknownFields {
824-
d.saveStrictError(fmt.Errorf("unknown field %q", key))
839+
d.saveStrictError(d.newFieldError("unknown field", string(key)))
825840
}
826841
}
827842

@@ -905,6 +920,8 @@ func (d *decodeState) object(v reflect.Value) error {
905920
d.errorContext.FieldStack = d.errorContext.FieldStack[:len(origErrorContext.FieldStack)]
906921
d.errorContext.Struct = origErrorContext.Struct
907922
}
923+
// Reset to original length and reuse the allocated space for the strict FieldStack slice.
924+
d.strictFieldStack = d.strictFieldStack[:origStrictFieldStackLen]
908925
if d.opcode == scanEndObject {
909926
break
910927
}
@@ -1141,6 +1158,12 @@ func (d *decodeState) valueInterface() (val interface{}) {
11411158

11421159
// arrayInterface is like array but returns []interface{}.
11431160
func (d *decodeState) arrayInterface() []interface{} {
1161+
origStrictFieldStackLen := len(d.strictFieldStack)
1162+
defer func() {
1163+
// Reset to original length and reuse the allocated space for the strict FieldStack slice.
1164+
d.strictFieldStack = d.strictFieldStack[:origStrictFieldStackLen]
1165+
}()
1166+
11441167
var v = make([]interface{}, 0)
11451168
for {
11461169
// Look ahead for ] - can only happen on first iteration.
@@ -1149,7 +1172,10 @@ func (d *decodeState) arrayInterface() []interface{} {
11491172
break
11501173
}
11511174

1175+
d.appendStrictFieldStackIndex(len(v))
11521176
v = append(v, d.valueInterface())
1177+
// Reset to original length and reuse the allocated space for the strict FieldStack slice.
1178+
d.strictFieldStack = d.strictFieldStack[:origStrictFieldStackLen]
11531179

11541180
// Next token must be , or ].
11551181
if d.opcode == scanSkipSpace {
@@ -1167,6 +1193,12 @@ func (d *decodeState) arrayInterface() []interface{} {
11671193

11681194
// objectInterface is like object but returns map[string]interface{}.
11691195
func (d *decodeState) objectInterface() map[string]interface{} {
1196+
origStrictFieldStackLen := len(d.strictFieldStack)
1197+
defer func() {
1198+
// Reset to original length and reuse the allocated space for the strict FieldStack slice.
1199+
d.strictFieldStack = d.strictFieldStack[:origStrictFieldStackLen]
1200+
}()
1201+
11701202
m := make(map[string]interface{})
11711203
for {
11721204
// Read opening " of string key or closing }.
@@ -1199,12 +1231,15 @@ func (d *decodeState) objectInterface() map[string]interface{} {
11991231

12001232
if d.disallowDuplicateFields {
12011233
if _, exists := m[key]; exists {
1202-
d.saveStrictError(fmt.Errorf("duplicate field %q", key))
1234+
d.saveStrictError(d.newFieldError("duplicate field", key))
12031235
}
12041236
}
12051237

12061238
// Read value.
1239+
d.appendStrictFieldStackKey(key)
12071240
m[key] = d.valueInterface()
1241+
// Reset to original length and reuse the allocated space for the strict FieldStack slice.
1242+
d.strictFieldStack = d.strictFieldStack[:origStrictFieldStackLen]
12081243

12091244
// Next token must be , or }.
12101245
if d.opcode == scanSkipSpace {

internal/golang/encoding/json/decode_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ var unmarshalTests = []unmarshalTest{
899899
"Q": 18
900900
}`,
901901
ptr: new(Top),
902-
err: fmt.Errorf("json: unknown field \"extra\""),
902+
err: fmt.Errorf("json: unknown field \"e.extra\""),
903903
disallowUnknownFields: true,
904904
},
905905
// issue 26444

internal/golang/encoding/json/kubernetes_patch.go

+28
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package json
1818

1919
import (
2020
gojson "encoding/json"
21+
"fmt"
22+
"strconv"
2123
"strings"
2224
)
2325

@@ -69,6 +71,14 @@ func (d *Decoder) DisallowDuplicateFields() {
6971
d.d.disallowDuplicateFields = true
7072
}
7173

74+
func (d *decodeState) newFieldError(msg, field string) error {
75+
if len(d.strictFieldStack) > 0 {
76+
return fmt.Errorf("%s %q", msg, strings.Join(d.strictFieldStack, "")+"."+field)
77+
} else {
78+
return fmt.Errorf("%s %q", msg, field)
79+
}
80+
}
81+
7282
// saveStrictError saves a strict decoding error,
7383
// for reporting at the end of the unmarshal if no other errors occurred.
7484
func (d *decodeState) saveStrictError(err error) {
@@ -90,6 +100,24 @@ func (d *decodeState) saveStrictError(err error) {
90100
d.savedStrictErrors = append(d.savedStrictErrors, err)
91101
}
92102

103+
func (d *decodeState) appendStrictFieldStackKey(key string) {
104+
if !d.disallowDuplicateFields && !d.disallowUnknownFields {
105+
return
106+
}
107+
if len(d.strictFieldStack) > 0 {
108+
d.strictFieldStack = append(d.strictFieldStack, ".", key)
109+
} else {
110+
d.strictFieldStack = append(d.strictFieldStack, key)
111+
}
112+
}
113+
114+
func (d *decodeState) appendStrictFieldStackIndex(i int) {
115+
if !d.disallowDuplicateFields && !d.disallowUnknownFields {
116+
return
117+
}
118+
d.strictFieldStack = append(d.strictFieldStack, "[", strconv.Itoa(i), "]")
119+
}
120+
93121
// UnmarshalStrictError holds errors resulting from use of strict disallow___ decoder directives.
94122
// If this is returned from Unmarshal(), it means the decoding was successful in all other respects.
95123
type UnmarshalStrictError struct {

internal/golang/encoding/json/kubernetes_patch_test.go

+35-11
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ func TestUnmarshalWithOptions(t *testing.T) {
9797
}
9898

9999
func TestStrictErrors(t *testing.T) {
100+
type SubType struct {
101+
}
100102
type Typed struct {
101-
A int `json:"a"`
103+
A int `json:"a"`
104+
B map[string]SubType `json:"b"`
105+
C []SubType `json:"c"`
102106
}
103107

104108
testcases := []struct {
@@ -111,21 +115,25 @@ func TestStrictErrors(t *testing.T) {
111115
name: "malformed 1",
112116
in: `{`,
113117
expectStrictErr: false,
118+
expectErr: `unexpected end of JSON input`,
114119
},
115120
{
116121
name: "malformed 2",
117122
in: `{}}`,
118123
expectStrictErr: false,
124+
expectErr: `invalid character '}' after top-level value`,
119125
},
120126
{
121127
name: "malformed 3",
122128
in: `{,}`,
123129
expectStrictErr: false,
130+
expectErr: `invalid character ',' looking for beginning of object key string`,
124131
},
125132
{
126133
name: "type error",
127134
in: `{"a":true}`,
128135
expectStrictErr: false,
136+
expectErr: `json: cannot unmarshal bool into Go struct field Typed.a of type int`,
129137
},
130138
{
131139
name: "unknown",
@@ -139,15 +147,23 @@ func TestStrictErrors(t *testing.T) {
139147
expectStrictErr: true,
140148
expectErr: `json: unknown field "unknown", unknown field "unknown2"`,
141149
},
150+
{
151+
name: "nested unknowns",
152+
in: `{"a":1,"unknown":true,"unknown2":true,"unknown":true,"unknown2":true,"b":{"a":{"unknown":true}},"c":[{"unknown":true},{"unknown":true}]}`,
153+
expectStrictErr: true,
154+
expectErr: `json: unknown field "unknown", unknown field "unknown2", unknown field "b.a.unknown", unknown field "c[0].unknown", unknown field "c[1].unknown"`,
155+
},
142156
{
143157
name: "unknowns and type error",
144158
in: `{"unknown":true,"a":true}`,
145159
expectStrictErr: false,
160+
expectErr: `json: cannot unmarshal bool into Go struct field Typed.a of type int`,
146161
},
147162
{
148163
name: "unknowns and malformed error",
149164
in: `{"unknown":true}}`,
150165
expectStrictErr: false,
166+
expectErr: `invalid character '}' after top-level value`,
151167
},
152168
}
153169

@@ -161,8 +177,8 @@ func TestStrictErrors(t *testing.T) {
161177
if tc.expectStrictErr != isStrictErr {
162178
t.Fatalf("expected strictErr=%v, got %v: %v", tc.expectStrictErr, isStrictErr, err)
163179
}
164-
if !strings.Contains(err.Error(), tc.expectErr) {
165-
t.Fatalf("expected error containing %q, got %q", tc.expectErr, err)
180+
if err.Error() != tc.expectErr {
181+
t.Fatalf("expected error\n%s\n%s", tc.expectErr, err)
166182
}
167183
t.Log(err)
168184
})
@@ -569,11 +585,16 @@ func TestPreserveInts(t *testing.T) {
569585
}
570586

571587
func TestDisallowDuplicateFields(t *testing.T) {
588+
type SubType struct {
589+
F int `json:"f"`
590+
G int `json:"g"`
591+
}
572592
type MixedObj struct {
573593
A int `json:"a"`
574594
B int `json:"b"`
575595
C int
576-
D map[string]string
596+
D map[string]string `json:"d"`
597+
E []SubType `json:"e"`
577598
}
578599
type SmallObj struct {
579600
F01 int
@@ -718,21 +739,21 @@ func TestDisallowDuplicateFields(t *testing.T) {
718739
}{
719740
{
720741
name: "duplicate typed",
721-
in: `{"a":1,"a":2,"a":3,"b":4,"b":5,"b":6,"C":7,"C":8,"C":9}`,
742+
in: `{"a":1,"a":2,"a":3,"b":4,"b":5,"b":6,"C":7,"C":8,"C":9,"e":[{"f":1,"f":1}]}`,
722743
to: &MixedObj{},
723-
expectErr: `json: duplicate field "a", duplicate field "b", duplicate field "C"`,
744+
expectErr: `json: duplicate field "a", duplicate field "b", duplicate field "C", duplicate field "e[0].f"`,
724745
},
725746
{
726747
name: "duplicate typed map field",
727748
in: `{"d":{"a":"","b":"","c":"","a":"","b":"","c":""}}`,
728749
to: &MixedObj{},
729-
expectErr: `json: duplicate field "a", duplicate field "b", duplicate field "c"`,
750+
expectErr: `json: duplicate field "d.a", duplicate field "d.b", duplicate field "d.c"`,
730751
},
731752
{
732753
name: "duplicate untyped map",
733-
in: `{"a":"","b":"","a":"","b":"","c":{"c":"","c":""}}`,
754+
in: `{"a":"","b":"","a":"","b":"","c":{"c":"","c":""},"d":{"e":{"f":1,"f":1}},"e":[{"g":1,"g":1}]}`,
734755
to: &map[string]interface{}{},
735-
expectErr: `json: duplicate field "a", duplicate field "b", duplicate field "c"`,
756+
expectErr: `json: duplicate field "a", duplicate field "b", duplicate field "c.c", duplicate field "d.e.f", duplicate field "e[0].g"`,
736757
},
737758
{
738759
name: "small obj",
@@ -754,8 +775,11 @@ func TestDisallowDuplicateFields(t *testing.T) {
754775
if (len(tc.expectErr) > 0) != (err != nil) {
755776
t.Fatalf("expected err=%v, got %v", len(tc.expectErr) > 0, err)
756777
}
757-
if len(tc.expectErr) > 0 && !strings.Contains(err.Error(), tc.expectErr) {
758-
t.Fatalf("expected err containing '%s', got %v", tc.expectErr, err)
778+
if err == nil {
779+
return
780+
}
781+
if err.Error() != tc.expectErr {
782+
t.Fatalf("expected err\n%s\ngot\n%s", tc.expectErr, err.Error())
759783
}
760784
})
761785
}

json_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ func TestUnmarshal(t *testing.T) {
103103
name: "duplicate map field",
104104
in: `{"c":{"a":"1","a":"2","b":"1","b":"2"}}`,
105105
to: func() interface{} { return &Obj{} },
106-
expect: &Obj{C: map[string]string{"a": "2", "b": "2"}}, // last duplicates win
107-
expectStrictErrs: []string{`duplicate field "a"`, `duplicate field "b"`}, // multiple strict errors are returned
106+
expect: &Obj{C: map[string]string{"a": "2", "b": "2"}}, // last duplicates win
107+
expectStrictErrs: []string{`duplicate field "c.a"`, `duplicate field "c.b"`}, // multiple strict errors are returned
108108
},
109109
{
110110
name: "unknown fields",

0 commit comments

Comments
 (0)