Skip to content

Commit

Permalink
Merge pull request #453 from dinhxuanvu/json-omitempty
Browse files Browse the repository at this point in the history
Add encoding option to specify how omitempty fields are encoded
  • Loading branch information
fxamacker committed Jan 7, 2024
2 parents f4ccee0 + f95914b commit 0cf56c3
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 16 deletions.
68 changes: 52 additions & 16 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,30 @@ func (m NilContainersMode) valid() bool {
return m >= 0 && m < maxNilContainersMode
}

// OmitEmptyMode specifies how to encode the fields with omitempty tag
// The default behavior omits if field value would encode as empty JSON value
type OmitEmptyMode int

const (
// OmitEmptyCBORValue specifies that fields tagged with "omitempty" should be
// omitted from encoding if the field would be encoded as an empty CBOR value,
// such as CBOR false, 0, 0.0, nil, empty byte, empty string, empty array,
// empty struct or empty map.
OmitEmptyCBORValue OmitEmptyMode = iota

// OmitEmptyGoValue specifies that fields tagged with "omitempty" should be
// omitted from encoding if the field has an empty Go value, defined as false, 0, a nil pointer,
// a nil interface value, and any empty array, slice, map, or string.
// This behavior is the same as the current (aka v1) encoding/json package included in Go.
OmitEmptyGoValue

maxOmitEmptyMode
)

func (om OmitEmptyMode) valid() bool {
return om >= 0 && om < maxOmitEmptyMode
}

// EncOptions specifies encoding options.
type EncOptions struct {
// Sort specifies sorting order.
Expand Down Expand Up @@ -326,6 +350,9 @@ type EncOptions struct {

// TagsMd specifies whether to allow CBOR tags (major type 6).
TagsMd TagsMode

// OmitEmptyMode specifies how to encode the fields with omitempty tag
OmitEmpty OmitEmptyMode
}

// CanonicalEncOptions returns EncOptions for "Canonical CBOR" encoding,
Expand Down Expand Up @@ -501,6 +528,9 @@ func (opts EncOptions) encMode() (*encMode, error) {
if opts.TagsMd == TagsForbidden && opts.TimeTag == EncTagRequired {
return nil, errors.New("cbor: cannot set TagsMd to TagsForbidden when TimeTag is EncTagRequired")
}
if !opts.OmitEmpty.valid() {
return nil, errors.New("cbor: invalid OmitEmpty " + strconv.Itoa(int(opts.OmitEmpty)))
}
em := encMode{
sort: opts.Sort,
shortestFloat: opts.ShortestFloat,
Expand All @@ -512,6 +542,7 @@ func (opts EncOptions) encMode() (*encMode, error) {
indefLength: opts.IndefLength,
nilContainers: opts.NilContainers,
tagsMd: opts.TagsMd,
omitEmpty: opts.OmitEmpty,
}
return &em, nil
}
Expand All @@ -535,6 +566,7 @@ type encMode struct {
indefLength IndefLengthMode
nilContainers NilContainersMode
tagsMd TagsMode
omitEmpty OmitEmptyMode
}

var defaultEncMode = &encMode{}
Expand All @@ -551,6 +583,7 @@ func (em *encMode) EncOptions() EncOptions {
TimeTag: em.timeTag,
IndefLength: em.indefLength,
TagsMd: em.tagsMd,
OmitEmpty: em.omitEmpty,
}
}

Expand Down Expand Up @@ -610,7 +643,7 @@ func putEncoderBuffer(e *encoderBuffer) {
}

type encodeFunc func(e *encoderBuffer, em *encMode, v reflect.Value) error
type isEmptyFunc func(v reflect.Value) (empty bool, err error)
type isEmptyFunc func(em *encMode, v reflect.Value) (empty bool, err error)

var (
cborFalse = []byte{0xf4}
Expand Down Expand Up @@ -1108,9 +1141,8 @@ func encodeStruct(e *encoderBuffer, em *encMode, v reflect.Value) (err error) {
continue
}
}

if f.omitEmpty {
empty, err := f.ief(fv)
empty, err := f.ief(em, fv)
if err != nil {
putEncoderBuffer(kve)
return err
Expand Down Expand Up @@ -1403,52 +1435,56 @@ func getEncodeIndirectValueFunc(t reflect.Type) encodeFunc {
}
}

func alwaysNotEmpty(_ reflect.Value) (empty bool, err error) {
func alwaysNotEmpty(em *encMode, _ reflect.Value) (empty bool, err error) {

Check failure on line 1438 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return false, nil
}

func isEmptyBool(v reflect.Value) (bool, error) {
func isEmptyBool(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1442 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return !v.Bool(), nil
}

func isEmptyInt(v reflect.Value) (bool, error) {
func isEmptyInt(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1446 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.Int() == 0, nil
}

func isEmptyUint(v reflect.Value) (bool, error) {
func isEmptyUint(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1450 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.Uint() == 0, nil
}

func isEmptyFloat(v reflect.Value) (bool, error) {
func isEmptyFloat(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1454 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.Float() == 0.0, nil
}

func isEmptyString(v reflect.Value) (bool, error) {
func isEmptyString(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1458 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.Len() == 0, nil
}

func isEmptySlice(v reflect.Value) (bool, error) {
func isEmptySlice(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1462 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.Len() == 0, nil
}

func isEmptyMap(v reflect.Value) (bool, error) {
func isEmptyMap(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1466 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.Len() == 0, nil
}

func isEmptyPtr(v reflect.Value) (bool, error) {
func isEmptyPtr(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1470 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.IsNil(), nil
}

func isEmptyIntf(v reflect.Value) (bool, error) {
func isEmptyIntf(em *encMode, v reflect.Value) (bool, error) {

Check failure on line 1474 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'em' seems to be unused, consider removing or renaming it as _ (revive)
return v.IsNil(), nil
}

func isEmptyStruct(v reflect.Value) (bool, error) {
func isEmptyStruct(em *encMode, v reflect.Value) (bool, error) {
structType, err := getEncodingStructType(v.Type())
if err != nil {
return false, err
}

if em.omitEmpty == OmitEmptyGoValue {
return false, nil
}

if structType.toArray {
return len(structType.fields) == 0, nil
}
Expand All @@ -1475,7 +1511,7 @@ func isEmptyStruct(v reflect.Value) (bool, error) {
}
}

empty, err := f.ief(fv)
empty, err := f.ief(em, fv)
if err != nil {
return false, err
}
Expand All @@ -1486,7 +1522,7 @@ func isEmptyStruct(v reflect.Value) (bool, error) {
return true, nil
}

func isEmptyBinaryMarshaler(v reflect.Value) (bool, error) {
func isEmptyBinaryMarshaler(em *encMode, v reflect.Value) (bool, error) {
m, ok := v.Interface().(encoding.BinaryMarshaler)
if !ok {
pv := reflect.New(v.Type())
Expand Down
65 changes: 65 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,71 @@ func TestOmitEmptyForStruct2(t *testing.T) {
testRoundTrip(t, []roundTripTest{{"non-default values", v, want}}, em, dm)
}

func TestOmitEmptyMode(t *testing.T) {
type T1 struct{}
type T struct {
B bool `cbor:"b"`
Bo bool `cbor:"bo,omitempty"`
UI uint `cbor:"ui"`
UIo uint `cbor:"uio,omitempty"`
I int `cbor:"i"`
Io int `cbor:"io,omitempty"`
F float64 `cbor:"f"`
Fo float64 `cbor:"fo,omitempty"`
S string `cbor:"s"`
So string `cbor:"so,omitempty"`
Slc []string `cbor:"slc"`
Slco []string `cbor:"slco,omitempty"`
M map[int]string `cbor:"m"`
Mo map[int]string `cbor:"mo,omitempty"`
P *int `cbor:"p"`
Po *int `cbor:"po,omitempty"`
Intf interface{} `cbor:"intf"`
Intfo interface{} `cbor:"intfo,omitempty"`
Str T1 `cbor:"str"`
Stro T1 `cbor:"stro,omitempty"`
}

v := T{}

// {"b": false, "ui": 0, "i":0, "f": 0, "s": "", "slc": nil, "m": nil, "p": nil, "intf": nil, "str": {}, "stro": {}}
wantGoValue := []byte{
0xab,
0x61, 0x62, 0xf4,
0x62, 0x75, 0x69, 0x00,
0x61, 0x69, 0x00,
0x61, 0x66, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x61, 0x73, 0x60,
0x63, 0x73, 0x6c, 0x63, 0xf6,
0x61, 0x6d, 0xf6,
0x61, 0x70, 0xf6,
0x64, 0x69, 0x6e, 0x74, 0x66, 0xf6,
0x63, 0x73, 0x74, 0x72, 0xa0,
0x64, 0x73, 0x74, 0x72, 0x6F, 0xa0,
}

// {"b": false, "ui": 0, "i":0, "f": 0, "s": "", "slc": nil, "m": nil, "p": nil, "intf": nil, "str": nil, "stro": nil}
wantCborValue := []byte{
0xaa,
0x61, 0x62, 0xf4,
0x62, 0x75, 0x69, 0x00,
0x61, 0x69, 0x00,
0x61, 0x66, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x61, 0x73, 0x60,
0x63, 0x73, 0x6c, 0x63, 0xf6,
0x61, 0x6d, 0xf6,
0x61, 0x70, 0xf6,
0x64, 0x69, 0x6e, 0x74, 0x66, 0xf6,
0x63, 0x73, 0x74, 0x72, 0xa0,
}

em_govalue, _ := EncOptions{OmitEmpty: OmitEmptyGoValue}.EncMode()
em_cborvalue, _ := EncOptions{OmitEmpty: OmitEmptyCBORValue}.EncMode()
dm, _ := DecOptions{}.DecMode()
testRoundTrip(t, []roundTripTest{{"OmitEmptyGoValue default values", v, wantGoValue}}, em_govalue, dm)
testRoundTrip(t, []roundTripTest{{"OmitEmptyCBORValue values", v, wantCborValue}}, em_cborvalue, dm)
}

func TestOmitEmptyForNestedStruct(t *testing.T) {
type T1 struct {
Bo bool `cbor:"bo,omitempty"`
Expand Down

0 comments on commit 0cf56c3

Please sign in to comment.