Skip to content

Commit

Permalink
Add support for json.Number. (#414)
Browse files Browse the repository at this point in the history
This commit adds support for encoding json.Number as a number
instead of as a string, which is the underlying type of
json.Number.

Care has been taken to not introduce a dependency on the
encoding/json package, by using an interface that specifies the
methods of json.Number instead of the datatype itself. This also
means that other packages that use a similar datatype will be
supported. (Like json-iterator)

This is useful for tools that wish to convert JSON data into YAML
data by deserializing JSON into a map[string]interface{}, and use
the json.Encoder's UseNumber() method, or structs that use the
json.Number datatype and also wish to support yaml.
  • Loading branch information
echlebek authored and niemeyer committed Nov 15, 2018
1 parent 5420a8b commit 51d6538
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 0 deletions.
8 changes: 8 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,14 @@ var unmarshalTests = []struct {
"---\nhello\n...\n}not yaml",
"hello",
},
{
"a: 5\n",
&struct{ A jsonNumberT }{"5"},
},
{
"a: 5.5\n",
&struct{ A jsonNumberT }{"5.5"},
},
}

type M map[interface{}]interface{}
Expand Down
28 changes: 28 additions & 0 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import (
"unicode/utf8"
)

// jsonNumber is the interface of the encoding/json.Number datatype.
// Repeating the interface here avoids a dependency on encoding/json, and also
// supports other libraries like jsoniter, which use a similar datatype with
// the same interface. Detecting this interface is useful when dealing with
// structures containing json.Number, which is a string under the hood. The
// encoder should prefer the use of Int64(), Float64() and string(), in that
// order, when encoding this type.
type jsonNumber interface {
Float64() (float64, error)
Int64() (int64, error)
String() string
}

type encoder struct {
emitter yaml_emitter_t
event yaml_event_t
Expand Down Expand Up @@ -89,6 +102,21 @@ func (e *encoder) marshal(tag string, in reflect.Value) {
}
iface := in.Interface()
switch m := iface.(type) {
case jsonNumber:
integer, err := m.Int64()
if err == nil {
// In this case the json.Number is a valid int64
in = reflect.ValueOf(integer)
break
}
float, err := m.Float64()
if err == nil {
// In this case the json.Number is a valid float64
in = reflect.ValueOf(float)
break
}
// fallback case - no number could be obtained
in = reflect.ValueOf(m.String())
case time.Time, *time.Time:
// Although time.Time implements TextMarshaler,
// we don't want to treat it as a string for YAML
Expand Down
30 changes: 30 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import (
"gopkg.in/yaml.v2"
)

type jsonNumberT string

func (j jsonNumberT) Int64() (int64, error) {
val, err := strconv.Atoi(string(j))
if err != nil {
return 0, err
}
return int64(val), nil
}

func (j jsonNumberT) Float64() (float64, error) {
return strconv.ParseFloat(string(j), 64)
}

func (j jsonNumberT) String() string {
return string(j)
}

var marshalIntTest = 123

var marshalTests = []struct {
Expand Down Expand Up @@ -367,6 +385,18 @@ var marshalTests = []struct {
map[string]string{"a": "你好 #comment"},
"a: '你好 #comment'\n",
},
{
map[string]interface{}{"a": jsonNumberT("5")},
"a: 5\n",
},
{
map[string]interface{}{"a": jsonNumberT("100.5")},
"a: 100.5\n",
},
{
map[string]interface{}{"a": jsonNumberT("bogus")},
"a: bogus\n",
},
}

func (s *S) TestMarshal(c *C) {
Expand Down

0 comments on commit 51d6538

Please sign in to comment.