Skip to content

Commit 5e70324

Browse files
committed
Convert numbers like Kubernetes does for json.Marshaler types
1 parent 1791273 commit 5e70324

File tree

2 files changed

+182
-5
lines changed

2 files changed

+182
-5
lines changed

value/reflectcache.go

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,23 @@ func (e TypeReflectCacheEntry) ToUnstructured(sv reflect.Value) (interface{}, er
210210

211211
case data[0] == '"':
212212
var result string
213-
err := json.Unmarshal(data, &result)
213+
err := unmarshal(data, &result)
214214
if err != nil {
215215
return nil, fmt.Errorf("error decoding string from json: %v", err)
216216
}
217217
return result, nil
218218

219219
case data[0] == '{':
220220
result := make(map[string]interface{})
221-
err := json.Unmarshal(data, &result)
221+
err := unmarshal(data, &result)
222222
if err != nil {
223223
return nil, fmt.Errorf("error decoding object from json: %v", err)
224224
}
225225
return result, nil
226226

227227
case data[0] == '[':
228228
result := make([]interface{}, 0)
229-
err := json.Unmarshal(data, &result)
229+
err := unmarshal(data, &result)
230230
if err != nil {
231231
return nil, fmt.Errorf("error decoding array from json: %v", err)
232232
}
@@ -238,9 +238,9 @@ func (e TypeReflectCacheEntry) ToUnstructured(sv reflect.Value) (interface{}, er
238238
resultFloat float64
239239
err error
240240
)
241-
if err = json.Unmarshal(data, &resultInt); err == nil {
241+
if err = unmarshal(data, &resultInt); err == nil {
242242
return resultInt, nil
243-
} else if err = json.Unmarshal(data, &resultFloat); err == nil {
243+
} else if err = unmarshal(data, &resultFloat); err == nil {
244244
return resultFloat, nil
245245
} else {
246246
return nil, fmt.Errorf("error decoding number from json: %v", err)
@@ -363,3 +363,101 @@ func (c *typeReflectCache) update(updates reflectCacheMap) {
363363
}
364364
c.value.Store(newCacheMap)
365365
}
366+
367+
// Below json Unmarshal is fromk8s.io/apimachinery/pkg/util/json
368+
// to handle number conversions as expected by Kubernetes
369+
370+
// limit recursive depth to prevent stack overflow errors
371+
const maxDepth = 10000
372+
373+
// unmarshal unmarshals the given data
374+
// If v is a *map[string]interface{}, numbers are converted to int64 or float64
375+
func unmarshal(data []byte, v interface{}) error {
376+
switch v := v.(type) {
377+
case *map[string]interface{}:
378+
// Build a decoder from the given data
379+
decoder := json.NewDecoder(bytes.NewBuffer(data))
380+
// Preserve numbers, rather than casting to float64 automatically
381+
decoder.UseNumber()
382+
// Run the decode
383+
if err := decoder.Decode(v); err != nil {
384+
return err
385+
}
386+
// If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64
387+
return convertMapNumbers(*v, 0)
388+
389+
case *[]interface{}:
390+
// Build a decoder from the given data
391+
decoder := json.NewDecoder(bytes.NewBuffer(data))
392+
// Preserve numbers, rather than casting to float64 automatically
393+
decoder.UseNumber()
394+
// Run the decode
395+
if err := decoder.Decode(v); err != nil {
396+
return err
397+
}
398+
// If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64
399+
return convertSliceNumbers(*v, 0)
400+
401+
default:
402+
return json.Unmarshal(data, v)
403+
}
404+
}
405+
406+
// convertMapNumbers traverses the map, converting any json.Number values to int64 or float64.
407+
// values which are map[string]interface{} or []interface{} are recursively visited
408+
func convertMapNumbers(m map[string]interface{}, depth int) error {
409+
if depth > maxDepth {
410+
return fmt.Errorf("exceeded max depth of %d", maxDepth)
411+
}
412+
413+
var err error
414+
for k, v := range m {
415+
switch v := v.(type) {
416+
case json.Number:
417+
m[k], err = convertNumber(v)
418+
case map[string]interface{}:
419+
err = convertMapNumbers(v, depth+1)
420+
case []interface{}:
421+
err = convertSliceNumbers(v, depth+1)
422+
}
423+
if err != nil {
424+
return err
425+
}
426+
}
427+
return nil
428+
}
429+
430+
// convertSliceNumbers traverses the slice, converting any json.Number values to int64 or float64.
431+
// values which are map[string]interface{} or []interface{} are recursively visited
432+
func convertSliceNumbers(s []interface{}, depth int) error {
433+
if depth > maxDepth {
434+
return fmt.Errorf("exceeded max depth of %d", maxDepth)
435+
}
436+
437+
var err error
438+
for i, v := range s {
439+
switch v := v.(type) {
440+
case json.Number:
441+
s[i], err = convertNumber(v)
442+
case map[string]interface{}:
443+
err = convertMapNumbers(v, depth+1)
444+
case []interface{}:
445+
err = convertSliceNumbers(v, depth+1)
446+
}
447+
if err != nil {
448+
return err
449+
}
450+
}
451+
return nil
452+
}
453+
454+
// convertNumber converts a json.Number to an int64 or float64, or returns an error
455+
func convertNumber(n json.Number) (interface{}, error) {
456+
// Attempt to convert to an int64 first
457+
if i, err := n.Int64(); err == nil {
458+
return i, nil
459+
}
460+
// Return a float64 (default json.Decode() behavior)
461+
// An overflow will return an error
462+
return n.Float64()
463+
}

value/reflectcache_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package value
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
)
23+
24+
type CustomValue struct {
25+
data []byte
26+
}
27+
28+
// MarshalJSON has a value receiver on this type.
29+
func (c CustomValue) MarshalJSON() ([]byte, error) {
30+
return c.data, nil
31+
}
32+
33+
type CustomPointer struct {
34+
data []byte
35+
}
36+
37+
// MarshalJSON has a pointer receiver on this type.
38+
func (c *CustomPointer) MarshalJSON() ([]byte, error) {
39+
return c.data, nil
40+
}
41+
42+
func TestToUnstructured(t *testing.T) {
43+
testcases := []struct {
44+
Data string
45+
Expected interface{}
46+
}{
47+
{Data: `null`, Expected: nil},
48+
{Data: `true`, Expected: true},
49+
{Data: `false`, Expected: false},
50+
{Data: `[]`, Expected: []interface{}{}},
51+
{Data: `[1]`, Expected: []interface{}{int64(1)}},
52+
{Data: `{}`, Expected: map[string]interface{}{}},
53+
{Data: `{"a":1}`, Expected: map[string]interface{}{"a": int64(1)}},
54+
{Data: `0`, Expected: int64(0)},
55+
{Data: `0.0`, Expected: float64(0)},
56+
}
57+
58+
for _, tc := range testcases {
59+
tc := tc
60+
t.Run(tc.Data, func(t *testing.T) {
61+
t.Parallel()
62+
custom := []interface{}{
63+
CustomValue{data: []byte(tc.Data)},
64+
&CustomValue{data: []byte(tc.Data)},
65+
&CustomPointer{data: []byte(tc.Data)},
66+
}
67+
for _, custom := range custom {
68+
rv := reflect.ValueOf(custom)
69+
result, err := TypeReflectEntryOf(rv.Type()).ToUnstructured(rv)
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
if !reflect.DeepEqual(result, tc.Expected) {
74+
t.Errorf("expected %#v but got %#v", tc.Expected, result)
75+
}
76+
}
77+
})
78+
}
79+
}

0 commit comments

Comments
 (0)