Skip to content

Commit d34c8ec

Browse files
authored
Merge pull request #644 from liggitt/omitzero
Add omitzero support
2 parents a2a5c5a + 69da12b commit d34c8ec

File tree

9 files changed

+920
-43
lines changed

9 files changed

+920
-43
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Codec passed multiple confidential security assessments in 2022. No vulnerabili
3939

4040
__🗜️  Data Size__
4141

42-
Struct tags (`toarray`, `keyasint`, `omitempty`) automatically reduce size of encoded structs. Encoding optionally shrinks float64→32→16 when values fit.
42+
Struct tags (`toarray`, `keyasint`, `omitempty`, `omitzero`) automatically reduce size of encoded structs. Encoding optionally shrinks float64→32→16 when values fit.
4343

4444
__:jigsaw:  Usability__
4545

@@ -147,6 +147,7 @@ We can write less code by using struct tags:
147147
- `toarray`: encode without field names (decode back to original struct)
148148
- `keyasint`: encode field names as integers (decode back to original struct)
149149
- `omitempty`: omit empty fields when encoding
150+
- `omitzero`: omit zero-value fields when encoding
150151
151152
![alt text](https://github.com/fxamacker/images/raw/master/cbor/v2.3.0/cbor_struct_tags_api.svg?sanitize=1 "CBOR API and Go Struct Tags")
152153
@@ -350,7 +351,7 @@ err = em.MarshalToBuffer(v, &buf) // encode v to provided buf
350351

351352
### Struct Tags
352353

353-
Struct tags (`toarray`, `keyasint`, `omitempty`) reduce encoded size of structs.
354+
Struct tags (`toarray`, `keyasint`, `omitempty`, `omitzero`) reduce encoded size of structs.
354355

355356
<details><summary> 🔎&nbsp; Example encoding 3-level nested Go struct to 1 byte CBOR</summary><p/>
356357

cache.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
type encodeFuncs struct {
1818
ef encodeFunc
1919
ief isEmptyFunc
20+
izf isZeroFunc
2021
}
2122

2223
var (
@@ -237,7 +238,7 @@ func getEncodingStructType(t reflect.Type) (*encodingStructType, error) {
237238
e := getEncodeBuffer()
238239
for i := 0; i < len(flds); i++ {
239240
// Get field's encodeFunc
240-
flds[i].ef, flds[i].ief = getEncodeFunc(flds[i].typ)
241+
flds[i].ef, flds[i].ief, flds[i].izf = getEncodeFunc(flds[i].typ)
241242
if flds[i].ef == nil {
242243
err = &UnsupportedTypeError{t}
243244
break
@@ -321,7 +322,7 @@ func getEncodingStructType(t reflect.Type) (*encodingStructType, error) {
321322
func getEncodingStructToArrayType(t reflect.Type, flds fields) (*encodingStructType, error) {
322323
for i := 0; i < len(flds); i++ {
323324
// Get field's encodeFunc
324-
flds[i].ef, flds[i].ief = getEncodeFunc(flds[i].typ)
325+
flds[i].ef, flds[i].ief, flds[i].izf = getEncodeFunc(flds[i].typ)
325326
if flds[i].ef == nil {
326327
structType := &encodingStructType{err: &UnsupportedTypeError{t}}
327328
encodingStructTypeCache.Store(t, structType)
@@ -337,14 +338,14 @@ func getEncodingStructToArrayType(t reflect.Type, flds fields) (*encodingStructT
337338
return structType, structType.err
338339
}
339340

340-
func getEncodeFunc(t reflect.Type) (encodeFunc, isEmptyFunc) {
341+
func getEncodeFunc(t reflect.Type) (encodeFunc, isEmptyFunc, isZeroFunc) {
341342
if v, _ := encodeFuncCache.Load(t); v != nil {
342343
fs := v.(encodeFuncs)
343-
return fs.ef, fs.ief
344+
return fs.ef, fs.ief, fs.izf
344345
}
345-
ef, ief := getEncodeFuncInternal(t)
346-
encodeFuncCache.Store(t, encodeFuncs{ef, ief})
347-
return ef, ief
346+
ef, ief, izf := getEncodeFuncInternal(t)
347+
encodeFuncCache.Store(t, encodeFuncs{ef, ief, izf})
348+
return ef, ief, izf
348349
}
349350

350351
func getTypeInfo(t reflect.Type) *typeInfo {

doc.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,14 @@ Decoding Options: https://github.com/fxamacker/cbor#decoding-options
111111
Struct tags like `cbor:"name,omitempty"` and `json:"name,omitempty"` work as expected.
112112
If both struct tags are specified then `cbor` is used.
113113
114-
Struct tags like "keyasint", "toarray", and "omitempty" make it easy to use
114+
Struct tags like "keyasint", "toarray", "omitempty", and "omitzero" make it easy to use
115115
very compact formats like COSE and CWT (CBOR Web Tokens) with structs.
116116
117+
The "omitzero" option omits zero values from encoding, matching
118+
[stdlib encoding/json behavior](https://pkg.go.dev/encoding/json#Marshal).
119+
When specified in the `cbor` tag, the option is always honored.
120+
When specified in the `json` tag, the option is honored when building with Go 1.24+.
121+
117122
For example, "toarray" makes struct fields encode to array elements. And "keyasint"
118123
makes struct fields encode to elements of CBOR map with int keys.
119124

encode.go

Lines changed: 135 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,19 @@ import (
5858
//
5959
// Marshal supports format string stored under the "cbor" key in the struct
6060
// field's tag. CBOR format string can specify the name of the field,
61-
// "omitempty" and "keyasint" options, and special case "-" for field omission.
62-
// If "cbor" key is absent, Marshal uses "json" key.
61+
// "omitempty", "omitzero" and "keyasint" options, and special case "-" for
62+
// field omission. If "cbor" key is absent, Marshal uses "json" key.
63+
// When using the "json" key, the "omitzero" option is honored when building
64+
// with Go 1.24+ to match stdlib encoding/json behavior.
6365
//
6466
// Struct field name is treated as integer if it has "keyasint" option in
6567
// its format string. The format string must specify an integer as its
6668
// field name.
6769
//
6870
// Special struct field "_" is used to specify struct level options, such as
6971
// "toarray". "toarray" option enables Go struct to be encoded as CBOR array.
70-
// "omitempty" is disabled by "toarray" to ensure that the same number
71-
// of elements are encoded every time.
72+
// "omitempty" and "omitzero" are disabled by "toarray" to ensure that the
73+
// same number of elements are encoded every time.
7274
//
7375
// Anonymous struct fields are marshaled as if their exported fields
7476
// were fields in the outer struct. Marshal follows the same struct fields
@@ -975,6 +977,7 @@ func putEncodeBuffer(e *bytes.Buffer) {
975977

976978
type encodeFunc func(e *bytes.Buffer, em *encMode, v reflect.Value) error
977979
type isEmptyFunc func(em *encMode, v reflect.Value) (empty bool, err error)
980+
type isZeroFunc func(v reflect.Value) (zero bool, err error)
978981

979982
func encode(e *bytes.Buffer, em *encMode, v reflect.Value) error {
980983
if !v.IsValid() {
@@ -983,7 +986,7 @@ func encode(e *bytes.Buffer, em *encMode, v reflect.Value) error {
983986
return nil
984987
}
985988
vt := v.Type()
986-
f, _ := getEncodeFunc(vt)
989+
f, _, _ := getEncodeFunc(vt)
987990
if f == nil {
988991
return &UnsupportedTypeError{vt}
989992
}
@@ -1483,6 +1486,15 @@ func encodeStruct(e *bytes.Buffer, em *encMode, v reflect.Value) (err error) {
14831486
continue
14841487
}
14851488
}
1489+
if f.omitZero {
1490+
zero, err := f.izf(fv)
1491+
if err != nil {
1492+
return err
1493+
}
1494+
if zero {
1495+
continue
1496+
}
1497+
}
14861498

14871499
if !f.keyAsInt && em.fieldName == FieldNameToByteString {
14881500
e.Write(f.cborNameByteString)
@@ -1775,32 +1787,32 @@ var (
17751787
typeByteString = reflect.TypeOf(ByteString(""))
17761788
)
17771789

1778-
func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc) {
1790+
func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc, izf isZeroFunc) {
17791791
k := t.Kind()
17801792
if k == reflect.Pointer {
1781-
return getEncodeIndirectValueFunc(t), isEmptyPtr
1793+
return getEncodeIndirectValueFunc(t), isEmptyPtr, getIsZeroFunc(t)
17821794
}
17831795
switch t {
17841796
case typeSimpleValue:
1785-
return encodeMarshalerType, isEmptyUint
1797+
return encodeMarshalerType, isEmptyUint, getIsZeroFunc(t)
17861798

17871799
case typeTag:
1788-
return encodeTag, alwaysNotEmpty
1800+
return encodeTag, alwaysNotEmpty, getIsZeroFunc(t)
17891801

17901802
case typeTime:
1791-
return encodeTime, alwaysNotEmpty
1803+
return encodeTime, alwaysNotEmpty, getIsZeroFunc(t)
17921804

17931805
case typeBigInt:
1794-
return encodeBigInt, alwaysNotEmpty
1806+
return encodeBigInt, alwaysNotEmpty, getIsZeroFunc(t)
17951807

17961808
case typeRawMessage:
1797-
return encodeMarshalerType, isEmptySlice
1809+
return encodeMarshalerType, isEmptySlice, getIsZeroFunc(t)
17981810

17991811
case typeByteString:
1800-
return encodeMarshalerType, isEmptyString
1812+
return encodeMarshalerType, isEmptyString, getIsZeroFunc(t)
18011813
}
18021814
if reflect.PointerTo(t).Implements(typeMarshaler) {
1803-
return encodeMarshalerType, alwaysNotEmpty
1815+
return encodeMarshalerType, alwaysNotEmpty, getIsZeroFunc(t)
18041816
}
18051817
if reflect.PointerTo(t).Implements(typeBinaryMarshaler) {
18061818
defer func() {
@@ -1815,63 +1827,63 @@ func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc) {
18151827
}
18161828
switch k {
18171829
case reflect.Bool:
1818-
return encodeBool, isEmptyBool
1830+
return encodeBool, isEmptyBool, getIsZeroFunc(t)
18191831

18201832
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
1821-
return encodeInt, isEmptyInt
1833+
return encodeInt, isEmptyInt, getIsZeroFunc(t)
18221834

18231835
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
1824-
return encodeUint, isEmptyUint
1836+
return encodeUint, isEmptyUint, getIsZeroFunc(t)
18251837

18261838
case reflect.Float32, reflect.Float64:
1827-
return encodeFloat, isEmptyFloat
1839+
return encodeFloat, isEmptyFloat, getIsZeroFunc(t)
18281840

18291841
case reflect.String:
1830-
return encodeString, isEmptyString
1842+
return encodeString, isEmptyString, getIsZeroFunc(t)
18311843

18321844
case reflect.Slice:
18331845
if t.Elem().Kind() == reflect.Uint8 {
1834-
return encodeByteString, isEmptySlice
1846+
return encodeByteString, isEmptySlice, getIsZeroFunc(t)
18351847
}
18361848
fallthrough
18371849

18381850
case reflect.Array:
1839-
f, _ := getEncodeFunc(t.Elem())
1851+
f, _, _ := getEncodeFunc(t.Elem())
18401852
if f == nil {
1841-
return nil, nil
1853+
return nil, nil, nil
18421854
}
1843-
return arrayEncodeFunc{f: f}.encode, isEmptySlice
1855+
return arrayEncodeFunc{f: f}.encode, isEmptySlice, getIsZeroFunc(t)
18441856

18451857
case reflect.Map:
18461858
f := getEncodeMapFunc(t)
18471859
if f == nil {
1848-
return nil, nil
1860+
return nil, nil, nil
18491861
}
1850-
return f, isEmptyMap
1862+
return f, isEmptyMap, getIsZeroFunc(t)
18511863

18521864
case reflect.Struct:
18531865
// Get struct's special field "_" tag options
18541866
if f, ok := t.FieldByName("_"); ok {
18551867
tag := f.Tag.Get("cbor")
18561868
if tag != "-" {
18571869
if hasToArrayOption(tag) {
1858-
return encodeStructToArray, isEmptyStruct
1870+
return encodeStructToArray, isEmptyStruct, isZeroFieldStruct
18591871
}
18601872
}
18611873
}
1862-
return encodeStruct, isEmptyStruct
1874+
return encodeStruct, isEmptyStruct, getIsZeroFunc(t)
18631875

18641876
case reflect.Interface:
1865-
return encodeIntf, isEmptyIntf
1877+
return encodeIntf, isEmptyIntf, getIsZeroFunc(t)
18661878
}
1867-
return nil, nil
1879+
return nil, nil, nil
18681880
}
18691881

18701882
func getEncodeIndirectValueFunc(t reflect.Type) encodeFunc {
18711883
for t.Kind() == reflect.Pointer {
18721884
t = t.Elem()
18731885
}
1874-
f, _ := getEncodeFunc(t)
1886+
f, _, _ := getEncodeFunc(t)
18751887
if f == nil {
18761888
return nil
18771889
}
@@ -1987,3 +1999,96 @@ func float32NaNFromReflectValue(v reflect.Value) float32 {
19871999
f32 := p.Convert(reflect.TypeOf((*float32)(nil))).Elem().Interface().(float32)
19882000
return f32
19892001
}
2002+
2003+
type isZeroer interface {
2004+
IsZero() bool
2005+
}
2006+
2007+
var isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
2008+
2009+
// getIsZeroFunc returns a function for the given type that can be called to determine if a given value is zero.
2010+
// Types that implement `IsZero() bool` are delegated to for non-nil values.
2011+
// Types that do not implement `IsZero() bool` use the reflect.Value#IsZero() implementation.
2012+
// The returned function matches behavior of stdlib encoding/json behavior in Go 1.24+.
2013+
func getIsZeroFunc(t reflect.Type) isZeroFunc {
2014+
// Provide a function that uses a type's IsZero method if defined.
2015+
switch {
2016+
case t == nil:
2017+
return isZeroDefault
2018+
case t.Kind() == reflect.Interface && t.Implements(isZeroerType):
2019+
return isZeroInterfaceCustom
2020+
case t.Kind() == reflect.Pointer && t.Implements(isZeroerType):
2021+
return isZeroPointerCustom
2022+
case t.Implements(isZeroerType):
2023+
return isZeroCustom
2024+
case reflect.PointerTo(t).Implements(isZeroerType):
2025+
return isZeroAddrCustom
2026+
default:
2027+
return isZeroDefault
2028+
}
2029+
}
2030+
2031+
// isZeroInterfaceCustom returns true for nil or pointer-to-nil values,
2032+
// and delegates to the custom IsZero() implementation otherwise.
2033+
func isZeroInterfaceCustom(v reflect.Value) (bool, error) {
2034+
kind := v.Kind()
2035+
2036+
switch kind {
2037+
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Interface, reflect.Slice:
2038+
if v.IsNil() {
2039+
return true, nil
2040+
}
2041+
}
2042+
2043+
switch kind {
2044+
case reflect.Interface, reflect.Pointer:
2045+
if elem := v.Elem(); elem.Kind() == reflect.Pointer && elem.IsNil() {
2046+
return true, nil
2047+
}
2048+
}
2049+
2050+
return v.Interface().(isZeroer).IsZero(), nil
2051+
}
2052+
2053+
// isZeroPointerCustom returns true for nil values,
2054+
// and delegates to the custom IsZero() implementation otherwise.
2055+
func isZeroPointerCustom(v reflect.Value) (bool, error) {
2056+
if v.IsNil() {
2057+
return true, nil
2058+
}
2059+
return v.Interface().(isZeroer).IsZero(), nil
2060+
}
2061+
2062+
// isZeroCustom delegates to the custom IsZero() implementation.
2063+
func isZeroCustom(v reflect.Value) (bool, error) {
2064+
return v.Interface().(isZeroer).IsZero(), nil
2065+
}
2066+
2067+
// isZeroAddrCustom delegates to the custom IsZero() implementation of the addr of the value.
2068+
func isZeroAddrCustom(v reflect.Value) (bool, error) {
2069+
if !v.CanAddr() {
2070+
// Temporarily box v so we can take the address.
2071+
v2 := reflect.New(v.Type()).Elem()
2072+
v2.Set(v)
2073+
v = v2
2074+
}
2075+
return v.Addr().Interface().(isZeroer).IsZero(), nil
2076+
}
2077+
2078+
// isZeroDefault calls reflect.Value#IsZero()
2079+
func isZeroDefault(v reflect.Value) (bool, error) {
2080+
if !v.IsValid() {
2081+
// v is zero value
2082+
return true, nil
2083+
}
2084+
return v.IsZero(), nil
2085+
}
2086+
2087+
// isZeroFieldStruct is used to determine whether to omit toarray structs
2088+
func isZeroFieldStruct(v reflect.Value) (bool, error) {
2089+
structType, err := getEncodingStructType(v.Type())
2090+
if err != nil {
2091+
return false, err
2092+
}
2093+
return len(structType.fields) == 0, nil
2094+
}

encode_map.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ func (me *mapKeyValueEncodeFunc) encodeKeyValues(e *bytes.Buffer, em *encMode, v
6565
}
6666

6767
func getEncodeMapFunc(t reflect.Type) encodeFunc {
68-
kf, _ := getEncodeFunc(t.Key())
69-
ef, _ := getEncodeFunc(t.Elem())
68+
kf, _, _ := getEncodeFunc(t.Key())
69+
ef, _, _ := getEncodeFunc(t.Elem())
7070
if kf == nil || ef == nil {
7171
return nil
7272
}

0 commit comments

Comments
 (0)