Skip to content

Commit f34835e

Browse files
committed
patch: add CaseSensitive decoder option
This adds a CaseSensitive decoder option (defaulting to false). When decoding a JSON object to a struct, JSON keys are treated case-sensitively when locating corresponding struct fields. Keys must exactly match `json` tag names (for tagged struct fields) or struct field names (for untagged struct fields), or are treated as unknown fields.
1 parent e166055 commit f34835e

File tree

3 files changed

+109
-1
lines changed

3 files changed

+109
-1
lines changed

internal/golang/encoding/json/decode.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ type decodeState struct {
234234

235235
savedStrictErrors []error
236236
seenStrictErrors map[string]struct{}
237+
238+
caseSensitive bool
237239
}
238240

239241
// readIndex returns the position of the last byte read.
@@ -720,7 +722,7 @@ func (d *decodeState) object(v reflect.Value) error {
720722
if i, ok := fields.nameIndex[string(key)]; ok {
721723
// Found an exact name match.
722724
f = &fields.list[i]
723-
} else {
725+
} else if !d.caseSensitive {
724726
// Fall back to the expensive case-insensitive
725727
// linear search.
726728
for i := range fields.list {

internal/golang/encoding/json/kubernetes_patch.go

+9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ func DisallowUnknownFields(d *decodeState) {
4040
d.disallowUnknownFields = true
4141
}
4242

43+
// CaseSensitive requires json keys to exactly match specified json tags (for tagged struct fields)
44+
// or struct field names (for untagged struct fields), or be treated as an unknown field.
45+
func CaseSensitive(d *decodeState) {
46+
d.caseSensitive = true
47+
}
48+
func (d *Decoder) CaseSensitive() {
49+
d.d.caseSensitive = true
50+
}
51+
4352
// saveStrictError saves a strict decoding error,
4453
// for reporting at the end of the unmarshal if no other errors occurred.
4554
func (d *decodeState) saveStrictError(err error) {

internal/golang/encoding/json/kubernetes_patch_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,100 @@ func TestStrictErrors(t *testing.T) {
165165
})
166166
}
167167
}
168+
169+
func TestCaseSensitive(t *testing.T) {
170+
type Embedded1 struct {
171+
C int `json:"c"`
172+
D int
173+
}
174+
type Embedded2 struct {
175+
E int `json:"e"`
176+
F int
177+
}
178+
179+
type Obj struct {
180+
A int `json:"a"`
181+
B int
182+
Embedded1 `json:",inline"`
183+
Embedded2
184+
}
185+
186+
testcases := []struct {
187+
name string
188+
in string
189+
to interface{}
190+
expect interface{}
191+
}{
192+
{
193+
name: "tagged",
194+
in: `{"A":"1","A":2,"a":3,"A":4,"A":"5"}`,
195+
to: &Obj{},
196+
expect: &Obj{A: 3},
197+
},
198+
{
199+
name: "untagged",
200+
in: `{"b":"1","b":2,"B":3,"b":4,"b":"5"}`,
201+
to: &Obj{},
202+
expect: &Obj{B: 3},
203+
},
204+
{
205+
name: "inline embedded tagged subfield",
206+
in: `{"C":"1","C":2,"c":3,"C":4,"C":"5"}`,
207+
to: &Obj{},
208+
expect: &Obj{Embedded1: Embedded1{C: 3}},
209+
},
210+
{
211+
name: "inline embedded untagged subfield",
212+
in: `{"d":"1","d":2,"D":3,"d":4,"d":"5"}`,
213+
to: &Obj{},
214+
expect: &Obj{Embedded1: Embedded1{D: 3}},
215+
},
216+
{
217+
name: "inline embedded field name",
218+
in: `{"Embedded1":{"c":3}}`,
219+
to: &Obj{},
220+
expect: &Obj{}, // inlined embedded is not addressable by field name
221+
},
222+
{
223+
name: "inline embedded empty name",
224+
in: `{"":{"c":3}}`,
225+
to: &Obj{},
226+
expect: &Obj{}, // inlined embedded is not addressable by empty json field name
227+
},
228+
{
229+
name: "untagged embedded tagged subfield",
230+
in: `{"E":"1","E":2,"e":3,"E":4,"E":"5"}`,
231+
to: &Obj{},
232+
expect: &Obj{Embedded2: Embedded2{E: 3}},
233+
},
234+
{
235+
name: "untagged embedded untagged subfield",
236+
in: `{"f":"1","f":2,"F":3,"f":4,"f":"5"}`,
237+
to: &Obj{},
238+
expect: &Obj{Embedded2: Embedded2{F: 3}},
239+
},
240+
{
241+
name: "untagged embedded field name",
242+
in: `{"Embedded2":{"e":3}}`,
243+
to: &Obj{},
244+
expect: &Obj{}, // untagged embedded is not addressable by field name
245+
},
246+
{
247+
name: "untagged embedded empty name",
248+
in: `{"":{"e":3}}`,
249+
to: &Obj{},
250+
expect: &Obj{}, // untagged embedded is not addressable by empty json field name
251+
},
252+
}
253+
254+
for _, tc := range testcases {
255+
t.Run(tc.name, func(t *testing.T) {
256+
if err := Unmarshal([]byte(tc.in), &tc.to, CaseSensitive); err != nil {
257+
t.Fatal(err)
258+
}
259+
if !reflect.DeepEqual(tc.expect, tc.to) {
260+
t.Fatalf("expected\n%#v\ngot\n%#v", tc.expect, tc.to)
261+
}
262+
})
263+
}
264+
}

0 commit comments

Comments
 (0)