Repository: fxamacker/cbor (go)

Issue:
feature: Add support for omitzero struct tag
Go 1.24 introduced the `omitzero` struct tag and it should be added here before fxamacker/cbor v2.8.0.

I'm taking some of my postponed Christmas break this week (March 24-28), so I can maybe add this feature during break.

Pull request description:
Add omitzero support
### Description

#### PR Was Proposed and Welcomed in Currently Open Issue

- [x] This PR was proposed and welcomed by maintainer(s) in issue #643
- [x] Closes #643

#### Checklist (for code PR only, ignore for docs PR)

- [x] Include unit tests that cover the new code
- [x] Pass all unit tests
- [x] Pass all lint checks in CI (goimports, gosec, staticcheck, etc.)
- [x] Sign each commit with your real name and email.  
      Last line of each commit message should be in this format:  
      Signed-off-by: Firstname Lastname <firstname.lastname@example.com>
- [x] Certify the Developer's Certificate of Origin 1.1
      (see next section).

#### Certify the Developer's Certificate of Origin 1.1

- [x] By marking this item as completed, I certify 
      the Developer Certificate of Origin 1.1.

```
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
```



Patch:
diff --git a/README.md b/README.md
index ce016f3..37e6615 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ Codec passed multiple confidential security assessments in 2022.  No vulnerabili
 
 __🗜️&nbsp; Data Size__
 
-Struct tags (`toarray`, `keyasint`, `omitempty`) automatically reduce size of encoded structs. Encoding optionally shrinks float64→32→16 when values fit.
+Struct tags (`toarray`, `keyasint`, `omitempty`, `omitzero`) automatically reduce size of encoded structs. Encoding optionally shrinks float64→32→16 when values fit.
 
 __:jigsaw:&nbsp; Usability__
 
@@ -147,6 +147,7 @@ We can write less code by using struct tags:
 - `toarray`: encode without field names (decode back to original struct)
 - `keyasint`: encode field names as integers (decode back to original struct)
 - `omitempty`: omit empty fields when encoding
+- `omitzero`: omit zero-value fields when encoding
 
 ![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")
 
@@ -350,7 +351,7 @@ err = em.MarshalToBuffer(v, &buf) // encode v to provided buf
 
 ### Struct Tags
 
-Struct tags (`toarray`, `keyasint`, `omitempty`) reduce encoded size of structs.
+Struct tags (`toarray`, `keyasint`, `omitempty`, `omitzero`) reduce encoded size of structs.
 
 <details><summary> 🔎&nbsp; Example encoding 3-level nested Go struct to 1 byte CBOR</summary><p/>
 
diff --git a/cache.go b/cache.go
index c18635a..8562fd8 100644
--- a/cache.go
+++ b/cache.go
@@ -17,6 +17,7 @@ import (
 type encodeFuncs struct {
 	ef  encodeFunc
 	ief isEmptyFunc
+	izf isZeroFunc
 }
 
 var (
@@ -237,7 +238,7 @@ func getEncodingStructType(t reflect.Type) (*encodingStructType, error) {
 	e := getEncodeBuffer()
 	for i := 0; i < len(flds); i++ {
 		// Get field's encodeFunc
-		flds[i].ef, flds[i].ief = getEncodeFunc(flds[i].typ)
+		flds[i].ef, flds[i].ief, flds[i].izf = getEncodeFunc(flds[i].typ)
 		if flds[i].ef == nil {
 			err = &UnsupportedTypeError{t}
 			break
@@ -321,7 +322,7 @@ func getEncodingStructType(t reflect.Type) (*encodingStructType, error) {
 func getEncodingStructToArrayType(t reflect.Type, flds fields) (*encodingStructType, error) {
 	for i := 0; i < len(flds); i++ {
 		// Get field's encodeFunc
-		flds[i].ef, flds[i].ief = getEncodeFunc(flds[i].typ)
+		flds[i].ef, flds[i].ief, flds[i].izf = getEncodeFunc(flds[i].typ)
 		if flds[i].ef == nil {
 			structType := &encodingStructType{err: &UnsupportedTypeError{t}}
 			encodingStructTypeCache.Store(t, structType)
@@ -337,14 +338,14 @@ func getEncodingStructToArrayType(t reflect.Type, flds fields) (*encodingStructT
 	return structType, structType.err
 }
 
-func getEncodeFunc(t reflect.Type) (encodeFunc, isEmptyFunc) {
+func getEncodeFunc(t reflect.Type) (encodeFunc, isEmptyFunc, isZeroFunc) {
 	if v, _ := encodeFuncCache.Load(t); v != nil {
 		fs := v.(encodeFuncs)
-		return fs.ef, fs.ief
+		return fs.ef, fs.ief, fs.izf
 	}
-	ef, ief := getEncodeFuncInternal(t)
-	encodeFuncCache.Store(t, encodeFuncs{ef, ief})
-	return ef, ief
+	ef, ief, izf := getEncodeFuncInternal(t)
+	encodeFuncCache.Store(t, encodeFuncs{ef, ief, izf})
+	return ef, ief, izf
 }
 
 func getTypeInfo(t reflect.Type) *typeInfo {
diff --git a/doc.go b/doc.go
index 23f68b9..201fbdd 100644
--- a/doc.go
+++ b/doc.go
@@ -111,9 +111,14 @@ Decoding Options: https://github.com/fxamacker/cbor#decoding-options
 Struct tags like `cbor:"name,omitempty"` and `json:"name,omitempty"` work as expected.
 If both struct tags are specified then `cbor` is used.
 
-Struct tags like "keyasint", "toarray", and "omitempty" make it easy to use
+Struct tags like "keyasint", "toarray", "omitempty", and "omitzero" make it easy to use
 very compact formats like COSE and CWT (CBOR Web Tokens) with structs.
 
+The "omitzero" option omits zero values from encoding, matching
+[stdlib encoding/json behavior](https://pkg.go.dev/encoding/json#Marshal).
+When specified in the `cbor` tag, the option is always honored.
+When specified in the `json` tag, the option is honored when building with Go 1.24+.
+
 For example, "toarray" makes struct fields encode to array elements.  And "keyasint"
 makes struct fields encode to elements of CBOR map with int keys.
 
diff --git a/encode.go b/encode.go
index 9fb7980..a3f9992 100644
--- a/encode.go
+++ b/encode.go
@@ -58,8 +58,10 @@ import (
 //
 // Marshal supports format string stored under the "cbor" key in the struct
 // field's tag.  CBOR format string can specify the name of the field,
-// "omitempty" and "keyasint" options, and special case "-" for field omission.
-// If "cbor" key is absent, Marshal uses "json" key.
+// "omitempty", "omitzero" and "keyasint" options, and special case "-" for
+// field omission. If "cbor" key is absent, Marshal uses "json" key.
+// When using the "json" key, the "omitzero" option is honored when building
+// with Go 1.24+ to match stdlib encoding/json behavior.
 //
 // Struct field name is treated as integer if it has "keyasint" option in
 // its format string.  The format string must specify an integer as its
@@ -67,8 +69,8 @@ import (
 //
 // Special struct field "_" is used to specify struct level options, such as
 // "toarray". "toarray" option enables Go struct to be encoded as CBOR array.
-// "omitempty" is disabled by "toarray" to ensure that the same number
-// of elements are encoded every time.
+// "omitempty" and "omitzero" are disabled by "toarray" to ensure that the
+// same number of elements are encoded every time.
 //
 // Anonymous struct fields are marshaled as if their exported fields
 // were fields in the outer struct.  Marshal follows the same struct fields
@@ -975,6 +977,7 @@ func putEncodeBuffer(e *bytes.Buffer) {
 
 type encodeFunc func(e *bytes.Buffer, em *encMode, v reflect.Value) error
 type isEmptyFunc func(em *encMode, v reflect.Value) (empty bool, err error)
+type isZeroFunc func(v reflect.Value) (zero bool, err error)
 
 func encode(e *bytes.Buffer, em *encMode, v reflect.Value) error {
 	if !v.IsValid() {
@@ -983,7 +986,7 @@ func encode(e *bytes.Buffer, em *encMode, v reflect.Value) error {
 		return nil
 	}
 	vt := v.Type()
-	f, _ := getEncodeFunc(vt)
+	f, _, _ := getEncodeFunc(vt)
 	if f == nil {
 		return &UnsupportedTypeError{vt}
 	}
@@ -1483,6 +1486,15 @@ func encodeStruct(e *bytes.Buffer, em *encMode, v reflect.Value) (err error) {
 				continue
 			}
 		}
+		if f.omitZero {
+			zero, err := f.izf(fv)
+			if err != nil {
+				return err
+			}
+			if zero {
+				continue
+			}
+		}
 
 		if !f.keyAsInt && em.fieldName == FieldNameToByteString {
 			e.Write(f.cborNameByteString)
@@ -1775,32 +1787,32 @@ var (
 	typeByteString      = reflect.TypeOf(ByteString(""))
 )
 
-func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc) {
+func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc, izf isZeroFunc) {
 	k := t.Kind()
 	if k == reflect.Pointer {
-		return getEncodeIndirectValueFunc(t), isEmptyPtr
+		return getEncodeIndirectValueFunc(t), isEmptyPtr, getIsZeroFunc(t)
 	}
 	switch t {
 	case typeSimpleValue:
-		return encodeMarshalerType, isEmptyUint
+		return encodeMarshalerType, isEmptyUint, getIsZeroFunc(t)
 
 	case typeTag:
-		return encodeTag, alwaysNotEmpty
+		return encodeTag, alwaysNotEmpty, getIsZeroFunc(t)
 
 	case typeTime:
-		return encodeTime, alwaysNotEmpty
+		return encodeTime, alwaysNotEmpty, getIsZeroFunc(t)
 
 	case typeBigInt:
-		return encodeBigInt, alwaysNotEmpty
+		return encodeBigInt, alwaysNotEmpty, getIsZeroFunc(t)
 
 	case typeRawMessage:
-		return encodeMarshalerType, isEmptySlice
+		return encodeMarshalerType, isEmptySlice, getIsZeroFunc(t)
 
 	case typeByteString:
-		return encodeMarshalerType, isEmptyString
+		return encodeMarshalerType, isEmptyString, getIsZeroFunc(t)
 	}
 	if reflect.PointerTo(t).Implements(typeMarshaler) {
-		return encodeMarshalerType, alwaysNotEmpty
+		return encodeMarshalerType, alwaysNotEmpty, getIsZeroFunc(t)
 	}
 	if reflect.PointerTo(t).Implements(typeBinaryMarshaler) {
 		defer func() {
@@ -1815,39 +1827,39 @@ func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc) {
 	}
 	switch k {
 	case reflect.Bool:
-		return encodeBool, isEmptyBool
+		return encodeBool, isEmptyBool, getIsZeroFunc(t)
 
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		return encodeInt, isEmptyInt
+		return encodeInt, isEmptyInt, getIsZeroFunc(t)
 
 	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-		return encodeUint, isEmptyUint
+		return encodeUint, isEmptyUint, getIsZeroFunc(t)
 
 	case reflect.Float32, reflect.Float64:
-		return encodeFloat, isEmptyFloat
+		return encodeFloat, isEmptyFloat, getIsZeroFunc(t)
 
 	case reflect.String:
-		return encodeString, isEmptyString
+		return encodeString, isEmptyString, getIsZeroFunc(t)
 
 	case reflect.Slice:
 		if t.Elem().Kind() == reflect.Uint8 {
-			return encodeByteString, isEmptySlice
+			return encodeByteString, isEmptySlice, getIsZeroFunc(t)
 		}
 		fallthrough
 
 	case reflect.Array:
-		f, _ := getEncodeFunc(t.Elem())
+		f, _, _ := getEncodeFunc(t.Elem())
 		if f == nil {
-			return nil, nil
+			return nil, nil, nil
 		}
-		return arrayEncodeFunc{f: f}.encode, isEmptySlice
+		return arrayEncodeFunc{f: f}.encode, isEmptySlice, getIsZeroFunc(t)
 
 	case reflect.Map:
 		f := getEncodeMapFunc(t)
 		if f == nil {
-			return nil, nil
+			return nil, nil, nil
 		}
-		return f, isEmptyMap
+		return f, isEmptyMap, getIsZeroFunc(t)
 
 	case reflect.Struct:
 		// Get struct's special field "_" tag options
@@ -1855,23 +1867,23 @@ func getEncodeFuncInternal(t reflect.Type) (ef encodeFunc, ief isEmptyFunc) {
 			tag := f.Tag.Get("cbor")
 			if tag != "-" {
 				if hasToArrayOption(tag) {
-					return encodeStructToArray, isEmptyStruct
+					return encodeStructToArray, isEmptyStruct, isZeroFieldStruct
 				}
 			}
 		}
-		return encodeStruct, isEmptyStruct
+		return encodeStruct, isEmptyStruct, getIsZeroFunc(t)
 
 	case reflect.Interface:
-		return encodeIntf, isEmptyIntf
+		return encodeIntf, isEmptyIntf, getIsZeroFunc(t)
 	}
-	return nil, nil
+	return nil, nil, nil
 }
 
 func getEncodeIndirectValueFunc(t reflect.Type) encodeFunc {
 	for t.Kind() == reflect.Pointer {
 		t = t.Elem()
 	}
-	f, _ := getEncodeFunc(t)
+	f, _, _ := getEncodeFunc(t)
 	if f == nil {
 		return nil
 	}
@@ -1987,3 +1999,96 @@ func float32NaNFromReflectValue(v reflect.Value) float32 {
 	f32 := p.Convert(reflect.TypeOf((*float32)(nil))).Elem().Interface().(float32)
 	return f32
 }
+
+type isZeroer interface {
+	IsZero() bool
+}
+
+var isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
+
+// getIsZeroFunc returns a function for the given type that can be called to determine if a given value is zero.
+// Types that implement `IsZero() bool` are delegated to for non-nil values.
+// Types that do not implement `IsZero() bool` use the reflect.Value#IsZero() implementation.
+// The returned function matches behavior of stdlib encoding/json behavior in Go 1.24+.
+func getIsZeroFunc(t reflect.Type) isZeroFunc {
+	// Provide a function that uses a type's IsZero method if defined.
+	switch {
+	case t == nil:
+		return isZeroDefault
+	case t.Kind() == reflect.Interface && t.Implements(isZeroerType):
+		return isZeroInterfaceCustom
+	case t.Kind() == reflect.Pointer && t.Implements(isZeroerType):
+		return isZeroPointerCustom
+	case t.Implements(isZeroerType):
+		return isZeroCustom
+	case reflect.PointerTo(t).Implements(isZeroerType):
+		return isZeroAddrCustom
+	default:
+		return isZeroDefault
+	}
+}
+
+// isZeroInterfaceCustom returns true for nil or pointer-to-nil values,
+// and delegates to the custom IsZero() implementation otherwise.
+func isZeroInterfaceCustom(v reflect.Value) (bool, error) {
+	kind := v.Kind()
+
+	switch kind {
+	case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Interface, reflect.Slice:
+		if v.IsNil() {
+			return true, nil
+		}
+	}
+
+	switch kind {
+	case reflect.Interface, reflect.Pointer:
+		if elem := v.Elem(); elem.Kind() == reflect.Pointer && elem.IsNil() {
+			return true, nil
+		}
+	}
+
+	return v.Interface().(isZeroer).IsZero(), nil
+}
+
+// isZeroPointerCustom returns true for nil values,
+// and delegates to the custom IsZero() implementation otherwise.
+func isZeroPointerCustom(v reflect.Value) (bool, error) {
+	if v.IsNil() {
+		return true, nil
+	}
+	return v.Interface().(isZeroer).IsZero(), nil
+}
+
+// isZeroCustom delegates to the custom IsZero() implementation.
+func isZeroCustom(v reflect.Value) (bool, error) {
+	return v.Interface().(isZeroer).IsZero(), nil
+}
+
+// isZeroAddrCustom delegates to the custom IsZero() implementation of the addr of the value.
+func isZeroAddrCustom(v reflect.Value) (bool, error) {
+	if !v.CanAddr() {
+		// Temporarily box v so we can take the address.
+		v2 := reflect.New(v.Type()).Elem()
+		v2.Set(v)
+		v = v2
+	}
+	return v.Addr().Interface().(isZeroer).IsZero(), nil
+}
+
+// isZeroDefault calls reflect.Value#IsZero()
+func isZeroDefault(v reflect.Value) (bool, error) {
+	if !v.IsValid() {
+		// v is zero value
+		return true, nil
+	}
+	return v.IsZero(), nil
+}
+
+// isZeroFieldStruct is used to determine whether to omit toarray structs
+func isZeroFieldStruct(v reflect.Value) (bool, error) {
+	structType, err := getEncodingStructType(v.Type())
+	if err != nil {
+		return false, err
+	}
+	return len(structType.fields) == 0, nil
+}
diff --git a/encode_map.go b/encode_map.go
index 3b60da7..2871bfd 100644
--- a/encode_map.go
+++ b/encode_map.go
@@ -65,8 +65,8 @@ func (me *mapKeyValueEncodeFunc) encodeKeyValues(e *bytes.Buffer, em *encMode, v
 }
 
 func getEncodeMapFunc(t reflect.Type) encodeFunc {
-	kf, _ := getEncodeFunc(t.Key())
-	ef, _ := getEncodeFunc(t.Elem())
+	kf, _, _ := getEncodeFunc(t.Key())
+	ef, _, _ := getEncodeFunc(t.Elem())
 	if kf == nil || ef == nil {
 		return nil
 	}
diff --git a/omitzero_go124.go b/omitzero_go124.go
new file mode 100644
index 0000000..c893a41
--- /dev/null
+++ b/omitzero_go124.go
@@ -0,0 +1,8 @@
+// Copyright (c) Faye Amacker. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+//go:build go1.24
+
+package cbor
+
+var jsonStdlibSupportsOmitzero = true
diff --git a/omitzero_pre_go124.go b/omitzero_pre_go124.go
new file mode 100644
index 0000000..db86a63
--- /dev/null
+++ b/omitzero_pre_go124.go
@@ -0,0 +1,8 @@
+// Copyright (c) Faye Amacker. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+//go:build !go1.24
+
+package cbor
+
+var jsonStdlibSupportsOmitzero = false
diff --git a/structfields.go b/structfields.go
index 429c6e2..593508d 100644
--- a/structfields.go
+++ b/structfields.go
@@ -18,9 +18,11 @@ type field struct {
 	typ                reflect.Type
 	ef                 encodeFunc
 	ief                isEmptyFunc
+	izf                isZeroFunc
 	typInfo            *typeInfo // used to decoder to reuse type info
 	tagged             bool      // used to choose dominant field (at the same level tagged fields dominate untagged fields)
 	omitEmpty          bool      // used to skip empty field
+	omitZero           bool      // used to skip zero field
 	keyAsInt           bool      // used to encode/decode field name as int
 }
 
@@ -165,9 +167,11 @@ func appendFields(
 			continue
 		}
 
+		cborTag := true
 		tag := f.Tag.Get("cbor")
 		if tag == "" {
 			tag = f.Tag.Get("json")
+			cborTag = false
 		}
 		if tag == "-" {
 			continue
@@ -177,7 +181,7 @@ func appendFields(
 
 		// Parse field tag options
 		var tagFieldName string
-		var omitempty, keyasint bool
+		var omitempty, omitzero, keyasint bool
 		for j := 0; tag != ""; j++ {
 			var token string
 			idx := strings.IndexByte(tag, ',')
@@ -192,6 +196,10 @@ func appendFields(
 				switch token {
 				case "omitempty":
 					omitempty = true
+				case "omitzero":
+					if cborTag || jsonStdlibSupportsOmitzero {
+						omitzero = true
+					}
 				case "keyasint":
 					keyasint = true
 				}
@@ -213,6 +221,7 @@ func appendFields(
 				idx:       fIdx,
 				typ:       f.Type,
 				omitEmpty: omitempty,
+				omitZero:  omitzero,
 				keyAsInt:  keyasint,
 				tagged:    tagged})
 		} else {


Test patch:
diff --git a/encode_test.go b/encode_test.go
index 79e9a8d..e1f34cc 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -11,6 +11,7 @@ import (
 	"math"
 	"math/big"
 	"reflect"
+	"runtime/debug"
 	"strings"
 	"testing"
 	"time"
@@ -1597,6 +1598,745 @@ func TestOmitEmptyForBigInt(t *testing.T) {
 	testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm)
 }
 
+func TestOmitZeroForBuiltinType(t *testing.T) {
+	type T struct {
+		B     bool           `cbor:"b"`
+		Bo    bool           `cbor:"bo,omitzero"`
+		UI    uint           `cbor:"ui"`
+		UIo   uint           `cbor:"uio,omitzero"`
+		I     int            `cbor:"i"`
+		Io    int            `cbor:"io,omitzero"`
+		F     float64        `cbor:"f"`
+		Fo    float64        `cbor:"fo,omitzero"`
+		S     string         `cbor:"s"`
+		So    string         `cbor:"so,omitzero"`
+		Slc   []string       `cbor:"slc"`
+		Slco  []string       `cbor:"slco,omitzero"`
+		M     map[int]string `cbor:"m"`
+		Mo    map[int]string `cbor:"mo,omitzero"`
+		P     *int           `cbor:"p"`
+		Po    *int           `cbor:"po,omitzero"`
+		Intf  any            `cbor:"intf"`
+		Intfo any            `cbor:"intfo,omitzero"`
+	}
+
+	v := T{}
+	// {"b": false, "ui": 0, "i":0, "f": 0, "s": "", "slc": null, "m": {}, "p": nil, "intf": nil }
+	want := []byte{0xa9,
+		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,
+	}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm)
+}
+
+func TestOmitZeroForAnonymousStruct(t *testing.T) {
+	type T struct {
+		Str  struct{} `cbor:"str"`
+		Stro struct{} `cbor:"stro,omitzero"`
+	}
+
+	v := T{}
+	want := []byte{0xa1, 0x63, 0x73, 0x74, 0x72, 0xa0} // {"str": {}}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm)
+}
+
+func TestOmitZeroForStruct1(t *testing.T) {
+	type T1 struct {
+		Bo    bool           `cbor:"bo,omitzero"`
+		UIo   uint           `cbor:"uio,omitzero"`
+		Io    int            `cbor:"io,omitzero"`
+		Fo    float64        `cbor:"fo,omitzero"`
+		So    string         `cbor:"so,omitzero"`
+		Slco  []string       `cbor:"slco,omitzero"`
+		Mo    map[int]string `cbor:"mo,omitzero"`
+		Po    *int           `cbor:"po,omitzero"`
+		Intfo any            `cbor:"intfo,omitzero"`
+	}
+	type T struct {
+		Str  T1 `cbor:"str"`
+		Stro T1 `cbor:"stro,omitzero"`
+	}
+
+	v := T{}
+	want := []byte{0xa1, 0x63, 0x73, 0x74, 0x72, 0xa0} // {"str": {}}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm)
+}
+
+func TestOmitZeroForStruct2(t *testing.T) {
+	type T1 struct {
+		Bo    bool           `cbor:"bo,omitzero"`
+		UIo   uint           `cbor:"uio,omitzero"`
+		Io    int            `cbor:"io,omitzero"`
+		Fo    float64        `cbor:"fo,omitzero"`
+		So    string         `cbor:"so,omitzero"`
+		Slco  []string       `cbor:"slco,omitzero"`
+		Mo    map[int]string `cbor:"mo,omitzero"`
+		Po    *int           `cbor:"po,omitzero"`
+		Intfo any            `cbor:"intfo"`
+	}
+	type T struct {
+		Stro T1 `cbor:"stro,omitzero"`
+	}
+
+	v := T{}
+	want := []byte{0xa0} // {}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"non-default values", v, want}}, em, dm)
+}
+
+func TestOmitZeroForNestedStruct(t *testing.T) {
+	type T1 struct {
+		Bo    bool           `cbor:"bo,omitzero"`
+		UIo   uint           `cbor:"uio,omitzero"`
+		Io    int            `cbor:"io,omitzero"`
+		Fo    float64        `cbor:"fo,omitzero"`
+		So    string         `cbor:"so,omitzero"`
+		Slco  []string       `cbor:"slco,omitzero"`
+		Mo    map[int]string `cbor:"mo,omitzero"`
+		Po    *int           `cbor:"po,omitzero"`
+		Intfo any            `cbor:"intfo,omitzero"`
+	}
+	type T2 struct {
+		Stro T1 `cbor:"stro,omitzero"`
+	}
+	type T struct {
+		Str  T2 `cbor:"str"`
+		Stro T2 `cbor:"stro,omitzero"`
+	}
+
+	v := T{}
+	want := []byte{0xa1, 0x63, 0x73, 0x74, 0x72, 0xa0} // {"str": {}}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm)
+}
+
+func TestOmitZeroForToArrayStruct1(t *testing.T) {
+	type T1 struct {
+		_    struct{} `cbor:",toarray"`
+		b    bool
+		ui   uint
+		i    int
+		f    float64
+		s    string
+		slc  []string
+		m    map[int]string
+		p    *int
+		intf any
+	}
+	type T struct {
+		Str  T1 `cbor:"str"`
+		Stro T1 `cbor:"stro,omitzero"`
+	}
+
+	v := T{
+		Str:  T1{b: false, ui: 0, i: 0, f: 0.0, s: "", slc: nil, m: nil, p: nil, intf: nil},
+		Stro: T1{b: false, ui: 0, i: 0, f: 0.0, s: "", slc: nil, m: nil, p: nil, intf: nil},
+	}
+	want := []byte{0xa1, 0x63, 0x73, 0x74, 0x72, 0x80} // {"str": []}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"no exportable fields", v, want}}, em, dm)
+}
+
+func TestOmitZeroForToArrayStruct2(t *testing.T) {
+	type T1 struct {
+		_     struct{}       `cbor:",toarray"`
+		Bo    bool           `cbor:"bo"`
+		UIo   uint           `cbor:"uio"`
+		Io    int            `cbor:"io"`
+		Fo    float64        `cbor:"fo"`
+		So    string         `cbor:"so"`
+		Slco  []string       `cbor:"slco"`
+		Mo    map[int]string `cbor:"mo"`
+		Po    *int           `cbor:"po"`
+		Intfo any            `cbor:"intfo"`
+	}
+	type T struct {
+		Stro T1 `cbor:"stro,omitzero"`
+	}
+
+	v := T{}
+	// {"stro": [false, 0, 0, 0.0, "", [], {}, nil, nil]}
+	want := []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0x89, 0xf4, 0x00, 0x00, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xf6, 0xf6, 0xf6, 0xf6}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"has exportable fields", v, want}}, em, dm)
+}
+
+func TestOmitZeroForStructWithPtrToAnonymousField(t *testing.T) {
+	type (
+		T1 struct {
+			X int `cbor:"x,omitzero"`
+			Y int `cbor:"y,omitzero"`
+		}
+		T2 struct {
+			*T1
+		}
+		T struct {
+			Stro T2 `cbor:"stro,omitzero"`
+		}
+	)
+
+	testCases := []struct {
+		name         string
+		obj          any
+		wantCborData []byte
+	}{
+		{
+			name:         "null pointer to anonymous field",
+			obj:          T{},
+			wantCborData: []byte{0xa0}, // {}
+		},
+		{
+			name:         "not-null pointer to anonymous field",
+			obj:          T{T2{&T1{}}},
+			wantCborData: []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0xa0}, // {"stro":{}}
+		},
+		{
+			name:         "not empty value in field 1",
+			obj:          T{T2{&T1{X: 1}}},
+			wantCborData: []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0xa1, 0x61, 0x78, 0x01}, // {stro:{x:1}}
+		},
+		{
+			name:         "not empty value in field 2",
+			obj:          T{T2{&T1{Y: 2}}},
+			wantCborData: []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0xa1, 0x61, 0x79, 0x02}, // {stro:{y:2}}
+		},
+		{
+			name:         "not empty value in all fields",
+			obj:          T{T2{&T1{X: 1, Y: 2}}},
+			wantCborData: []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0xa2, 0x61, 0x78, 0x01, 0x61, 0x79, 0x02}, // {stro:{x:1, y:2}}
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			b, err := Marshal(tc.obj)
+			if err != nil {
+				t.Errorf("Marshal(%+v) returned error %v", tc.obj, err)
+			}
+			if !bytes.Equal(b, tc.wantCborData) {
+				t.Errorf("Marshal(%+v) = 0x%x, want 0x%x", tc.obj, b, tc.wantCborData)
+			}
+		})
+	}
+}
+
+func TestOmitZeroForStructWithAnonymousField(t *testing.T) {
+	type (
+		T1 struct {
+			X int `cbor:"x,omitzero"`
+			Y int `cbor:"y,omitzero"`
+		}
+		T2 struct {
+			T1
+		}
+		T struct {
+			Stro T2 `cbor:"stro,omitzero"`
+		}
+	)
+
+	testCases := []struct {
+		name         string
+		obj          any
+		wantCborData []byte
+	}{
+		{
+			name:         "default values",
+			obj:          T{},
+			wantCborData: []byte{0xa0}, // {}
+		},
+		{
+			name:         "default values",
+			obj:          T{T2{T1{}}},
+			wantCborData: []byte{0xa0}, // {}
+		},
+		{
+			name:         "not empty value in field 1",
+			obj:          T{T2{T1{X: 1}}},
+			wantCborData: []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0xa1, 0x61, 0x78, 0x01}, // {stro:{x:1}}
+		},
+		{
+			name:         "not empty value in field 2",
+			obj:          T{T2{T1{Y: 2}}},
+			wantCborData: []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0xa1, 0x61, 0x79, 0x02}, // {stro:{y:2}}
+		},
+		{
+			name:         "not empty value in all fields",
+			obj:          T{T2{T1{X: 1, Y: 2}}},
+			wantCborData: []byte{0xa1, 0x64, 0x73, 0x74, 0x72, 0x6f, 0xa2, 0x61, 0x78, 0x01, 0x61, 0x79, 0x02}, // {stro:{x:1, y:2}}
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			b, err := Marshal(tc.obj)
+			if err != nil {
+				t.Errorf("Marshal(%+v) returned error %v", tc.obj, err)
+			}
+			if !bytes.Equal(b, tc.wantCborData) {
+				t.Errorf("Marshal(%+v) = 0x%x, want 0x%x", tc.obj, b, tc.wantCborData)
+			}
+		})
+	}
+}
+
+func TestOmitZeroForBinaryMarshaler1(t *testing.T) {
+	type T1 struct {
+		No number `cbor:"no,omitzero"`
+	}
+	type T struct {
+		Str  T1 `cbor:"str"`
+		Stro T1 `cbor:"stro,omitzero"`
+	}
+
+	testCases := []roundTripTest{
+		{
+			"empty BinaryMarshaler",
+			T1{},
+			[]byte{0xa0}, // {}
+		},
+		{
+			"empty struct containing empty BinaryMarshaler",
+			T{},
+			[]byte{0xa1, 0x63, 0x73, 0x74, 0x72, 0xa0}, // {str: {}}
+		},
+	}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, testCases, em, dm)
+}
+
+func TestOmitZeroForBinaryMarshaler2(t *testing.T) {
+	type T1 struct {
+		So stru `cbor:"so,omitzero"`
+	}
+	type T struct {
+		Str  T1 `cbor:"str"`
+		Stro T1 `cbor:"stro,omitzero"`
+	}
+
+	testCases := []roundTripTest{
+		{
+			"empty BinaryMarshaler",
+			T1{},
+			[]byte{0xa0}, // {}
+		},
+		{
+			"empty struct containing empty BinaryMarshaler",
+			T{},
+			[]byte{0xa1, 0x63, 0x73, 0x74, 0x72, 0xa0}, // {str: {}}
+		},
+	}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, testCases, em, dm)
+}
+
+func TestOmitZeroForTime(t *testing.T) {
+	type T struct {
+		Tm time.Time `cbor:"t,omitzero"`
+	}
+
+	v := T{}
+	want := []byte{0xa0} // {}
+
+	em, _ := EncOptions{}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm)
+}
+
+func TestOmitZeroForBigInt(t *testing.T) {
+	type T struct {
+		I big.Int `cbor:"bi,omitzero"`
+	}
+
+	v := T{}
+	want := []byte{0xa0} // {}
+
+	em, _ := EncOptions{BigIntConvert: BigIntConvertNone}.EncMode()
+	dm, _ := DecOptions{}.DecMode()
+	testRoundTrip(t, []roundTripTest{{"default values", v, want}}, em, dm)
+}
+
+func TestIsZero(t *testing.T) {
+	var zeroStructZeroer isZeroer = zeroTestTypeCustom{value: 42}
+
+	testcases := []struct {
+		name string
+		t    reflect.Type
+		v    reflect.Value
+
+		expect    bool
+		expectErr bool
+	}{
+		{
+			name:   "nil",
+			t:      reflect.TypeOf(nil),
+			v:      reflect.ValueOf(nil),
+			expect: true,
+		},
+		{
+			name:   "string-zero",
+			t:      reflect.TypeOf(""),
+			v:      reflect.ValueOf(""),
+			expect: true,
+		},
+
+		{
+			name:   "string-nonzero",
+			t:      reflect.TypeOf(""),
+			v:      reflect.ValueOf("a"),
+			expect: false,
+		},
+		{
+			name:   "int-zero",
+			t:      reflect.TypeOf(0),
+			v:      reflect.ValueOf(0),
+			expect: true,
+		},
+		{
+			name:   "int-nonzero",
+			t:      reflect.TypeOf(0),
+			v:      reflect.ValueOf(1),
+			expect: false,
+		},
+
+		{
+			name:   "bool-zero",
+			t:      reflect.TypeOf(false),
+			v:      reflect.ValueOf(false),
+			expect: true,
+		},
+		{
+			name:   "bool-nonzero",
+			t:      reflect.TypeOf(false),
+			v:      reflect.ValueOf(true),
+			expect: false,
+		},
+
+		{
+			name:   "slice-zero",
+			t:      reflect.TypeOf([]string(nil)),
+			v:      reflect.ValueOf([]string(nil)),
+			expect: true,
+		},
+		{
+			name:   "slice-nonzero",
+			t:      reflect.TypeOf([]string(nil)),
+			v:      reflect.ValueOf([]string{}),
+			expect: false,
+		},
+
+		{
+			name:   "map-zero",
+			t:      reflect.TypeOf(map[string]string(nil)),
+			v:      reflect.ValueOf(map[string]string(nil)),
+			expect: true,
+		},
+		{
+			name:   "map-nonzero",
+			t:      reflect.TypeOf(map[string]string(nil)),
+			v:      reflect.ValueOf(map[string]string{}),
+			expect: false,
+		},
+
+		{
+			name:   "struct-zero",
+			t:      reflect.TypeOf(zeroTestType{}),
+			v:      reflect.ValueOf(zeroTestType{}),
+			expect: true,
+		},
+		{
+			name:   "struct-nonzero",
+			t:      reflect.TypeOf(zeroTestType{}),
+			v:      reflect.ValueOf(zeroTestType{value: 42}),
+			expect: false,
+		},
+
+		{
+			name:   "pointer-zero",
+			t:      reflect.TypeOf((*zeroTestType)(nil)),
+			v:      reflect.ValueOf((*zeroTestType)(nil)),
+			expect: true,
+		},
+		{
+			name:   "pointer-nonzero",
+			t:      reflect.TypeOf((*zeroTestType)(nil)),
+			v:      reflect.ValueOf(&zeroTestType{}),
+			expect: false,
+		},
+
+		{
+			name:   "any-struct-zero",
+			t:      reflect.TypeOf(any(nil)),
+			v:      reflect.ValueOf(zeroTestType{}),
+			expect: true,
+		},
+		{
+			name:   "any-struct-nonzero",
+			t:      reflect.TypeOf(any(nil)),
+			v:      reflect.ValueOf(zeroTestType{value: 42}),
+			expect: false,
+		},
+
+		{
+			name:   "any-pointer-zero",
+			t:      reflect.TypeOf(any(nil)),
+			v:      reflect.ValueOf((*zeroTestType)(nil)),
+			expect: true,
+		},
+		{
+			name:   "any-pointer-nonzero",
+			t:      reflect.TypeOf(any(nil)),
+			v:      reflect.ValueOf(&zeroTestType{}),
+			expect: false,
+		},
+
+		{
+			name:   "custom-structreceiver-zero-structvalue",
+			t:      reflect.TypeOf(zeroTestTypeCustom{}),
+			v:      reflect.ValueOf(zeroTestTypeCustom{value: 42}),
+			expect: true,
+		},
+		{
+			name:   "custom-structreceiver-nonzero-structvalue",
+			t:      reflect.TypeOf(zeroTestTypeCustom{}),
+			v:      reflect.ValueOf(zeroTestTypeCustom{value: 1}),
+			expect: false,
+		},
+		{
+			name:   "custom-structreceiver-zero-pointervalue",
+			t:      reflect.TypeOf(zeroTestTypeCustom{}),
+			v:      reflect.ValueOf(&zeroTestTypeCustom{value: 42}),
+			expect: true,
+		},
+		{
+			name:   "custom-structreceiver-nonzero-pointervalue",
+			t:      reflect.TypeOf(zeroTestTypeCustom{}),
+			v:      reflect.ValueOf(&zeroTestTypeCustom{value: 1}),
+			expect: false,
+		},
+
+		{
+			name:   "custom-structreceiver-zero-pointervalue",
+			t:      reflect.TypeOf(&zeroTestTypeCustom{}),
+			v:      reflect.ValueOf(&zeroTestTypeCustom{value: 42}),
+			expect: true,
+		},
+		{
+			name:   "custom-structreceiver-nonzero-pointervalue",
+			t:      reflect.TypeOf(&zeroTestTypeCustom{}),
+			v:      reflect.ValueOf(&zeroTestTypeCustom{value: 1}),
+			expect: false,
+		},
+		{
+			name:   "custom-structreceiver-zero-nil-pointervalue",
+			t:      reflect.TypeOf(&zeroTestTypeCustom{}),
+			v:      reflect.ValueOf((*zeroTestTypeCustom)(nil)),
+			expect: true,
+		},
+
+		{
+			name:   "custom-pointerreceiver-zero-structvalue",
+			t:      reflect.TypeOf(zeroTestTypeCustomPointer{}),
+			v:      reflect.ValueOf(zeroTestTypeCustomPointer{value: 42}),
+			expect: true,
+		},
+		{
+			name:   "custom-pointerreceiver-nonzero-structvalue",
+			t:      reflect.TypeOf(zeroTestTypeCustomPointer{}),
+			v:      reflect.ValueOf(zeroTestTypeCustomPointer{value: 1}),
+			expect: false,
+		},
+
+		{
+			name:   "custom-pointerreceiver-zero-pointervalue",
+			t:      reflect.TypeOf(&zeroTestTypeCustomPointer{}),
+			v:      reflect.ValueOf(&zeroTestTypeCustomPointer{value: 42}),
+			expect: true,
+		},
+		{
+			name:   "custom-pointerreceiver-nonzero-pointervalue",
+			t:      reflect.TypeOf(&zeroTestTypeCustomPointer{}),
+			v:      reflect.ValueOf(&zeroTestTypeCustomPointer{value: 1}),
+			expect: false,
+		},
+
+		{
+			name:   "custom-interface-nil-pointer",
+			t:      isZeroerType,
+			v:      reflect.ValueOf((*zeroTestTypeCustom)(nil)),
+			expect: true,
+		},
+		{
+			name:   "custom-interface-zero-structreceiver-pointer",
+			t:      isZeroerType,
+			v:      reflect.ValueOf(&zeroTestTypeCustom{value: 42}),
+			expect: true,
+		},
+		{
+			name:   "custom-interface-zero-structreceiver",
+			t:      isZeroerType,
+			v:      reflect.ValueOf(zeroStructZeroer),
+			expect: true,
+		},
+		{
+			name:   "custom-interface-nonzero-struct",
+			t:      isZeroerType,
+			v:      reflect.ValueOf(&zeroTestTypeCustom{value: 1}),
+			expect: false,
+		},
+		{
+			name:   "custom-interface-nil-pointerreceiver",
+			t:      isZeroerType,
+			v:      reflect.ValueOf((*zeroTestTypeCustomPointer)(nil)),
+			expect: true,
+		},
+		{
+			name:   "custom-interface-zero-pointerreceiver",
+			t:      isZeroerType,
+			v:      reflect.ValueOf(&zeroTestTypeCustomPointer{value: 42}),
+			expect: true,
+		},
+		{
+			name:   "custom-interface-nonzero-pointerreceiver",
+			t:      isZeroerType,
+			v:      reflect.ValueOf(&zeroTestTypeCustomPointer{value: 1}),
+			expect: false,
+		},
+	}
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			defer func() {
+				if err := recover(); err != nil {
+					t.Log(string(debug.Stack()))
+					t.Errorf("unexpected panic %v", err)
+				}
+			}()
+			got, err := getIsZeroFunc(tc.t)(tc.v)
+			if tc.expectErr != (err != nil) {
+				t.Errorf("got err=%v, expected %v", err, tc.expectErr)
+			}
+			if tc.expect != got {
+				t.Errorf("got %v, expected %v", got, tc.expect)
+			}
+		})
+	}
+}
+
+type zeroTestType struct {
+	value int
+}
+
+type zeroTestTypeCustom struct {
+	value int
+}
+
+func (z zeroTestTypeCustom) IsZero() bool {
+	return z.value == 42
+}
+
+type zeroTestTypeCustomPointer struct {
+	value int
+}
+
+func (z *zeroTestTypeCustomPointer) IsZero() bool {
+	return z.value == 42
+}
+
+func TestJSONStdlibOmitZero(t *testing.T) {
+	type CBOR struct {
+		S string `cbor:"s,omitzero"`
+	}
+	type JSON struct {
+		S string `json:"s,omitzero"`
+	}
+
+	testcases := []struct {
+		name   string
+		stdlib bool
+		obj    any
+		want   []byte
+	}{
+		{
+			name:   "cbor-stdlib-off",
+			stdlib: false,
+			obj:    CBOR{},
+			want:   []byte{0xa0}, // {}
+		},
+		{
+			name:   "cbor-stdlib-on",
+			stdlib: true,
+			obj:    CBOR{},
+			want:   []byte{0xa0}, // {}
+		},
+		{
+			name:   "json-stdlib-off",
+			stdlib: false,
+			obj:    JSON{},
+			want:   []byte{0xa1, 0x61, 0x73, 0x60}, // {"s":""}
+		},
+		{
+			name:   "json-stdlib-on",
+			stdlib: true,
+			obj:    JSON{},
+			want:   []byte{0xa0}, // {}
+		},
+	}
+
+	original := jsonStdlibSupportsOmitzero
+	reset := func() {
+		// reset to original
+		jsonStdlibSupportsOmitzero = original
+		// clear type caches
+		encodingStructTypeCache.Range(func(key, _ any) bool {
+			encodingStructTypeCache.Delete(key)
+			return true
+		})
+		typeInfoCache.Range(func(key, _ any) bool {
+			typeInfoCache.Delete(key)
+			return true
+		})
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			reset()
+			jsonStdlibSupportsOmitzero = tc.stdlib
+			t.Cleanup(reset)
+
+			em, _ := EncOptions{}.EncMode()
+			dm, _ := DecOptions{}.DecMode()
+			testRoundTrip(t, []roundTripTest{{tc.name, tc.obj, tc.want}}, em, dm)
+		})
+	}
+}
+
 func TestTaggedField(t *testing.T) {
 	// A field (T2.X) with a tag dominates untagged field.
 	type (