Support custom IsZero() methods with omitzero tag (#1020)
The omitzero tag now respects custom IsZero() methods on types, similar to how encoding/json handles this. Previously, only reflect.Value.IsZero() was used, which ignores user-defined implementations. Fixes #1003 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+22
-1
@@ -390,7 +390,28 @@ func shouldOmitEmpty(options valueOptions, v reflect.Value) bool {
|
||||
}
|
||||
|
||||
func shouldOmitZero(options valueOptions, v reflect.Value) bool {
|
||||
return options.omitzero && v.IsZero()
|
||||
if !options.omitzero {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the type implements isZeroer interface (has a custom IsZero method).
|
||||
if v.Type().Implements(isZeroerType) {
|
||||
return v.Interface().(isZeroer).IsZero()
|
||||
}
|
||||
|
||||
// Check if pointer type implements isZeroer.
|
||||
if reflect.PointerTo(v.Type()).Implements(isZeroerType) {
|
||||
if v.CanAddr() {
|
||||
return v.Addr().Interface().(isZeroer).IsZero()
|
||||
}
|
||||
// Create a temporary addressable copy to call the pointer receiver method.
|
||||
pv := reflect.New(v.Type())
|
||||
pv.Elem().Set(v)
|
||||
return pv.Interface().(isZeroer).IsZero()
|
||||
}
|
||||
|
||||
// Fall back to reflect's IsZero for types without custom IsZero method.
|
||||
return v.IsZero()
|
||||
}
|
||||
|
||||
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
|
||||
|
||||
@@ -1195,6 +1195,286 @@ IP = '192.168.178.35'
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
// customZeroType has a custom IsZero method that returns true
|
||||
// when Value is less than 10.
|
||||
type customZeroType struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
func (c customZeroType) IsZero() bool {
|
||||
return c.Value < 10
|
||||
}
|
||||
|
||||
// customZeroPointerType has a custom IsZero method on the pointer receiver.
|
||||
type customZeroPointerType struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
func (c *customZeroPointerType) IsZero() bool {
|
||||
return c.Value < 10
|
||||
}
|
||||
|
||||
func TestEncoderOmitzeroCustomIsZero(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroType `toml:",omitzero"`
|
||||
Normal int `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Custom.Value = 5, which is < 10, so custom IsZero returns true
|
||||
d := doc{
|
||||
Custom: customZeroType{Value: 5},
|
||||
Normal: 0,
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Both fields should be omitted: Custom because custom IsZero returns true,
|
||||
// Normal because its reflect zero value is true.
|
||||
expected := ``
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
func TestEncoderOmitzeroCustomIsZeroNotZero(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroType `toml:",omitzero"`
|
||||
Normal int `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Custom.Value = 15, which is >= 10, so custom IsZero returns false
|
||||
d := doc{
|
||||
Custom: customZeroType{Value: 15},
|
||||
Normal: 42,
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Both fields should be present
|
||||
expected := `Normal = 42
|
||||
|
||||
[Custom]
|
||||
Value = 15
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
func TestEncoderOmitzeroCustomIsZeroPointerReceiver(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroPointerType `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Custom.Value = 5, which is < 10, so custom IsZero returns true
|
||||
d := doc{
|
||||
Custom: customZeroPointerType{Value: 5},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Field should be omitted because custom IsZero returns true
|
||||
expected := ``
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
func TestEncoderOmitzeroCustomIsZeroPointerReceiverNotZero(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroPointerType `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Custom.Value = 15, which is >= 10, so custom IsZero returns false
|
||||
d := doc{
|
||||
Custom: customZeroPointerType{Value: 15},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Field should be present
|
||||
expected := `[Custom]
|
||||
Value = 15
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
// TestEncoderOmitzeroCustomIsZeroPointerReceiverAddressable tests the v.CanAddr() path
|
||||
// by marshaling a pointer to a struct, which makes fields addressable.
|
||||
func TestEncoderOmitzeroCustomIsZeroPointerReceiverAddressable(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroPointerType `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Custom.Value = 5, which is < 10, so custom IsZero returns true
|
||||
d := &doc{
|
||||
Custom: customZeroPointerType{Value: 5},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Field should be omitted because custom IsZero returns true
|
||||
expected := ``
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
// TestEncoderOmitzeroCustomIsZeroPointerReceiverAddressableNotZero tests the v.CanAddr() path
|
||||
// when custom IsZero returns false.
|
||||
func TestEncoderOmitzeroCustomIsZeroPointerReceiverAddressableNotZero(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroPointerType `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Custom.Value = 15, which is >= 10, so custom IsZero returns false
|
||||
d := &doc{
|
||||
Custom: customZeroPointerType{Value: 15},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Field should be present
|
||||
expected := `[Custom]
|
||||
Value = 15
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
// TestEncoderOmitzeroCustomIsZeroInlineTable tests omitzero with inline tables.
|
||||
func TestEncoderOmitzeroCustomIsZeroInlineTable(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroType `toml:",omitzero,inline"`
|
||||
}
|
||||
|
||||
// Custom.Value = 5, which is < 10, so custom IsZero returns true
|
||||
d := doc{
|
||||
Custom: customZeroType{Value: 5},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Field should be omitted
|
||||
expected := ``
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
// TestEncoderOmitzeroCustomIsZeroInlineTableNotZero tests omitzero with inline tables when not zero.
|
||||
func TestEncoderOmitzeroCustomIsZeroInlineTableNotZero(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroType `toml:",omitzero,inline"`
|
||||
}
|
||||
|
||||
// Custom.Value = 15, which is >= 10, so custom IsZero returns false
|
||||
d := doc{
|
||||
Custom: customZeroType{Value: 15},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Field should be present as inline table
|
||||
expected := `Custom = {Value = 15}
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
// TestEncoderOmitzeroCustomIsZeroMixedTypes tests omitzero with a mix of custom and regular types.
|
||||
func TestEncoderOmitzeroCustomIsZeroMixedTypes(t *testing.T) {
|
||||
type doc struct {
|
||||
Custom customZeroType `toml:",omitzero"`
|
||||
Regular int `toml:",omitzero"`
|
||||
NoOmit customZeroType `toml:""`
|
||||
Pointer *int `toml:",omitzero"`
|
||||
}
|
||||
|
||||
d := doc{
|
||||
Custom: customZeroType{Value: 5}, // IsZero returns true
|
||||
Regular: 0, // zero value
|
||||
NoOmit: customZeroType{Value: 5}, // not omitted (no omitzero tag)
|
||||
Pointer: nil, // nil pointer
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Custom is omitted (custom IsZero true), Regular is omitted (zero value),
|
||||
// NoOmit is present (no omitzero tag), Pointer is omitted (nil)
|
||||
expected := `[NoOmit]
|
||||
Value = 5
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
// TestEncoderOmitzeroCustomIsZeroSlice tests omitzero with slices containing custom types.
|
||||
func TestEncoderOmitzeroCustomIsZeroSlice(t *testing.T) {
|
||||
type doc struct {
|
||||
Items []customZeroType `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Nil slice should be omitted (IsZero returns true for nil slices)
|
||||
d := doc{
|
||||
Items: nil,
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := ``
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
|
||||
// Empty but non-nil slice is NOT zero, so it's included
|
||||
d2 := doc{
|
||||
Items: []customZeroType{},
|
||||
}
|
||||
|
||||
b2, err := toml.Marshal(d2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected2 := `Items = []
|
||||
`
|
||||
|
||||
assert.Equal(t, expected2, string(b2))
|
||||
}
|
||||
|
||||
// TestEncoderOmitzeroCustomIsZeroNestedStruct tests omitzero with nested structs.
|
||||
func TestEncoderOmitzeroCustomIsZeroNestedStruct(t *testing.T) {
|
||||
type inner struct {
|
||||
Custom customZeroType `toml:",omitzero"`
|
||||
Value int `toml:",omitzero"`
|
||||
}
|
||||
type doc struct {
|
||||
Inner inner `toml:",omitzero"`
|
||||
}
|
||||
|
||||
// Inner struct has all zero fields, but the struct itself is not zero
|
||||
// (reflect.Value.IsZero checks if all fields are zero)
|
||||
d := doc{
|
||||
Inner: inner{
|
||||
Custom: customZeroType{Value: 5}, // custom IsZero returns true
|
||||
Value: 0, // zero value
|
||||
},
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(d)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Inner is present but its fields are omitted
|
||||
expected := `[Inner]
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, string(b))
|
||||
}
|
||||
|
||||
func TestEncoderTagFieldName(t *testing.T) {
|
||||
type doc struct {
|
||||
String string `toml:"hello"`
|
||||
|
||||
@@ -6,10 +6,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// isZeroer is used to check if a type has a custom IsZero method.
|
||||
// This allows custom types to define their own zero-value semantics.
|
||||
type isZeroer interface {
|
||||
IsZero() bool
|
||||
}
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||
isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
|
||||
mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
|
||||
sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
|
||||
stringType = reflect.TypeOf("")
|
||||
|
||||
Reference in New Issue
Block a user