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 {
|
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) {
|
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))
|
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) {
|
func TestEncoderTagFieldName(t *testing.T) {
|
||||||
type doc struct {
|
type doc struct {
|
||||||
String string `toml:"hello"`
|
String string `toml:"hello"`
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ import (
|
|||||||
"time"
|
"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 (
|
var (
|
||||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||||
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||||
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||||
|
isZeroerType = reflect.TypeOf((*isZeroer)(nil)).Elem()
|
||||||
mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
|
mapStringInterfaceType = reflect.TypeOf(map[string]interface{}(nil))
|
||||||
sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
|
sliceInterfaceType = reflect.TypeOf([]interface{}(nil))
|
||||||
stringType = reflect.TypeOf("")
|
stringType = reflect.TypeOf("")
|
||||||
|
|||||||
Reference in New Issue
Block a user