Skip to content

Commit

Permalink
Preserve int/float distinction when decoding raw values
Browse files Browse the repository at this point in the history
Kubernetes-commit: 2e5b76c339838808915ffb6d68c158a73a8d06cb
  • Loading branch information
liggitt authored and k8s-publishing-bot committed Apr 3, 2020
1 parent f336d9b commit 62af470
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 32 deletions.
25 changes: 25 additions & 0 deletions pkg/util/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,36 @@ func Unmarshal(data []byte, v interface{}) error {
// If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64
return convertSliceNumbers(*v, 0)

case *interface{}:
// Build a decoder from the given data
decoder := json.NewDecoder(bytes.NewBuffer(data))
// Preserve numbers, rather than casting to float64 automatically
decoder.UseNumber()
// Run the decode
if err := decoder.Decode(v); err != nil {
return err
}
// If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64
return convertInterfaceNumbers(v, 0)

default:
return json.Unmarshal(data, v)
}
}

func convertInterfaceNumbers(v *interface{}, depth int) error {
var err error
switch v2 := (*v).(type) {
case json.Number:
*v, err = convertNumber(v2)
case map[string]interface{}:
err = convertMapNumbers(v2, depth+1)
case []interface{}:
err = convertSliceNumbers(v2, depth+1)
}
return err
}

// convertMapNumbers traverses the map, converting any json.Number values to int64 or float64.
// values which are map[string]interface{} or []interface{} are recursively visited
func convertMapNumbers(m map[string]interface{}, depth int) error {
Expand Down
163 changes: 131 additions & 32 deletions pkg/util/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ limitations under the License.
package json

import (
gojson "encoding/json"

"fmt"
"math"
"reflect"
Expand Down Expand Up @@ -278,42 +280,139 @@ func TestEvaluateTypes(t *testing.T) {
},
}

for _, tc := range testCases {
inputJSON := fmt.Sprintf(`{"data":%s}`, tc.In)
expectedJSON := fmt.Sprintf(`{"data":%s}`, tc.Out)
m := map[string]interface{}{}
err := Unmarshal([]byte(inputJSON), &m)
if tc.Err && err != nil {
// Expected error
continue
}
if err != nil {
t.Errorf("%s: error decoding: %v", tc.In, err)
continue
}
if tc.Err {
t.Errorf("%s: expected error, got none", tc.In)
continue
}
data, ok := m["data"]
if !ok {
t.Errorf("%s: decoded object missing data key: %#v", tc.In, m)
continue
}
if !reflect.DeepEqual(tc.Data, data) {
t.Errorf("%s: expected\n\t%#v (%v), got\n\t%#v (%v)", tc.In, tc.Data, reflect.TypeOf(tc.Data), data, reflect.TypeOf(data))
continue
for i, tc := range testCases {
t.Run(fmt.Sprintf("%d_map", i), func(t *testing.T) {
// decode the input as a map item
inputJSON := fmt.Sprintf(`{"data":%s}`, tc.In)
expectedJSON := fmt.Sprintf(`{"data":%s}`, tc.Out)
m := map[string]interface{}{}
err := Unmarshal([]byte(inputJSON), &m)
if tc.Err && err != nil {
// Expected error
return
}
if err != nil {
t.Fatalf("%s: error decoding: %v", tc.In, err)
}
if tc.Err {
t.Fatalf("%s: expected error, got none", tc.In)
}
data, ok := m["data"]
if !ok {
t.Fatalf("%s: decoded object missing data key: %#v", tc.In, m)
}
if !reflect.DeepEqual(tc.Data, data) {
t.Fatalf("%s: expected\n\t%#v (%v), got\n\t%#v (%v)", tc.In, tc.Data, reflect.TypeOf(tc.Data), data, reflect.TypeOf(data))
}

outputJSON, err := Marshal(m)
if err != nil {
t.Fatalf("%s: error encoding: %v", tc.In, err)
}

if expectedJSON != string(outputJSON) {
t.Fatalf("%s: expected\n\t%s, got\n\t%s", tc.In, expectedJSON, string(outputJSON))
}
})

t.Run(fmt.Sprintf("%d_slice", i), func(t *testing.T) {
// decode the input as an array item
inputJSON := fmt.Sprintf(`[0,%s]`, tc.In)
expectedJSON := fmt.Sprintf(`[0,%s]`, tc.Out)
m := []interface{}{}
err := Unmarshal([]byte(inputJSON), &m)
if tc.Err && err != nil {
// Expected error
return
}
if err != nil {
t.Fatalf("%s: error decoding: %v", tc.In, err)
}
if tc.Err {
t.Fatalf("%s: expected error, got none", tc.In)
}
if len(m) != 2 {
t.Fatalf("%s: decoded object wasn't the right length: %#v", tc.In, m)
}
data := m[1]
if !reflect.DeepEqual(tc.Data, data) {
t.Fatalf("%s: expected\n\t%#v (%v), got\n\t%#v (%v)", tc.In, tc.Data, reflect.TypeOf(tc.Data), data, reflect.TypeOf(data))
}

outputJSON, err := Marshal(m)
if err != nil {
t.Fatalf("%s: error encoding: %v", tc.In, err)
}

if expectedJSON != string(outputJSON) {
t.Fatalf("%s: expected\n\t%s, got\n\t%s", tc.In, expectedJSON, string(outputJSON))
}
})

t.Run(fmt.Sprintf("%d_raw", i), func(t *testing.T) {
// decode the input as a standalone object
inputJSON := fmt.Sprintf(`%s`, tc.In)
expectedJSON := fmt.Sprintf(`%s`, tc.Out)
var m interface{}
err := Unmarshal([]byte(inputJSON), &m)
if tc.Err && err != nil {
// Expected error
return
}
if err != nil {
t.Fatalf("%s: error decoding: %v", tc.In, err)
}
if tc.Err {
t.Fatalf("%s: expected error, got none", tc.In)
}
data := m
if !reflect.DeepEqual(tc.Data, data) {
t.Fatalf("%s: expected\n\t%#v (%v), got\n\t%#v (%v)", tc.In, tc.Data, reflect.TypeOf(tc.Data), data, reflect.TypeOf(data))
}

outputJSON, err := Marshal(m)
if err != nil {
t.Fatalf("%s: error encoding: %v", tc.In, err)
}

if expectedJSON != string(outputJSON) {
t.Fatalf("%s: expected\n\t%s, got\n\t%s", tc.In, expectedJSON, string(outputJSON))
}
})
}
}

func TestUnmarshalNil(t *testing.T) {
{
var v *interface{}
err := Unmarshal([]byte(`0`), v)
goerr := gojson.Unmarshal([]byte(`0`), v)
if err == nil || goerr == nil || err.Error() != goerr.Error() {
t.Fatalf("expected error matching stdlib, got %v, %v", err, goerr)
} else {
t.Log(err)
}
}

outputJSON, err := Marshal(m)
if err != nil {
t.Errorf("%s: error encoding: %v", tc.In, err)
continue
{
var v *[]interface{}
err := Unmarshal([]byte(`[]`), v)
goerr := gojson.Unmarshal([]byte(`[]`), v)
if err == nil || goerr == nil || err.Error() != goerr.Error() {
t.Fatalf("expected error matching stdlib, got %v, %v", err, goerr)
} else {
t.Log(err)
}
}

if expectedJSON != string(outputJSON) {
t.Errorf("%s: expected\n\t%s, got\n\t%s", tc.In, expectedJSON, string(outputJSON))
continue
{
var v *map[string]interface{}
err := Unmarshal([]byte(`{}`), v)
goerr := gojson.Unmarshal([]byte(`{}`), v)
if err == nil || goerr == nil || err.Error() != goerr.Error() {
t.Fatalf("expected error matching stdlib, got %v, %v", err, goerr)
} else {
t.Log(err)
}
}
}

0 comments on commit 62af470

Please sign in to comment.