Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion marshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
280 changes: 280 additions & 0 deletions marshaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
7 changes: 7 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand Down