Skip to content
Draft
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ json.Unmarshal(input, &data)

[More documentation](http://jsoniter.com/migrate-from-go-std.html)

# Struct Tags

## omitzero

The `omitzero` tag option omits a field if it is the zero value of its type. Compatible with Go 1.24+'s `encoding/json`.

- Calls `IsZero() bool` method if implemented (e.g., `time.Time`)
- Empty slices/maps are NOT omitted (unlike `omitempty`)

```go
type Config struct {
CreatedAt time.Time `json:"created_at,omitzero"` // omitted when zero time
Count int `json:"count,omitzero"` // omitted when 0
Items []string `json:"items,omitzero"` // omitted when nil, NOT when empty
}
```

# How to get

```
Expand Down
23 changes: 18 additions & 5 deletions reflect_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,11 @@ func describeStruct(ctx *ctx, typ reflect2.Type) *StructDescriptor {
structDescriptor := describeStruct(ctx, field.Type())
for _, binding := range structDescriptor.Fields {
binding.levels = append([]int{i}, binding.levels...)
omitempty := binding.Encoder.(*structFieldEncoder).omitempty
binding.Encoder = &structFieldEncoder{field, binding.Encoder, omitempty}
oldEncoder := binding.Encoder.(*structFieldEncoder)
omitempty := oldEncoder.omitempty
omitzero := oldEncoder.omitzero
zeroChecker := oldEncoder.zeroChecker
binding.Encoder = &structFieldEncoder{field, binding.Encoder, omitempty, omitzero, zeroChecker}
binding.Decoder = &structFieldDecoder{field, binding.Decoder}
embeddedBindings = append(embeddedBindings, binding)
}
Expand All @@ -362,9 +365,12 @@ func describeStruct(ctx *ctx, typ reflect2.Type) *StructDescriptor {
structDescriptor := describeStruct(ctx, ptrType.Elem())
for _, binding := range structDescriptor.Fields {
binding.levels = append([]int{i}, binding.levels...)
omitempty := binding.Encoder.(*structFieldEncoder).omitempty
oldEncoder := binding.Encoder.(*structFieldEncoder)
omitempty := oldEncoder.omitempty
omitzero := oldEncoder.omitzero
zeroChecker := oldEncoder.zeroChecker
binding.Encoder = &dereferenceEncoder{binding.Encoder}
binding.Encoder = &structFieldEncoder{field, binding.Encoder, omitempty}
binding.Encoder = &structFieldEncoder{field, binding.Encoder, omitempty, omitzero, zeroChecker}
binding.Decoder = &dereferenceDecoder{ptrType.Elem(), binding.Decoder}
binding.Decoder = &structFieldDecoder{field, binding.Decoder}
embeddedBindings = append(embeddedBindings, binding)
Expand Down Expand Up @@ -443,10 +449,13 @@ func (bindings sortableBindings) Swap(i, j int) {
func processTags(structDescriptor *StructDescriptor, cfg *frozenConfig) {
for _, binding := range structDescriptor.Fields {
shouldOmitEmpty := false
shouldOmitZero := false
tagParts := strings.Split(binding.Field.Tag().Get(cfg.getTagKey()), ",")
for _, tagPart := range tagParts[1:] {
if tagPart == "omitempty" {
shouldOmitEmpty = true
} else if tagPart == "omitzero" {
shouldOmitZero = true
} else if tagPart == "string" {
if binding.Field.Type().Kind() == reflect.String {
binding.Decoder = &stringModeStringDecoder{binding.Decoder, cfg}
Expand All @@ -458,7 +467,11 @@ func processTags(structDescriptor *StructDescriptor, cfg *frozenConfig) {
}
}
binding.Decoder = &structFieldDecoder{binding.Field, binding.Decoder}
binding.Encoder = &structFieldEncoder{binding.Field, binding.Encoder, shouldOmitEmpty}
var zeroChecker checkIsZero
if shouldOmitZero {
zeroChecker = createCheckIsZero(nil, binding.Field.Type())
}
binding.Encoder = &structFieldEncoder{binding.Field, binding.Encoder, shouldOmitEmpty, shouldOmitZero, zeroChecker}
}
}

Expand Down
234 changes: 233 additions & 1 deletion reflect_struct_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ package jsoniter

import (
"fmt"
"github.com/modern-go/reflect2"
"io"
"reflect"
"unsafe"

"github.com/modern-go/reflect2"
)

// checkIsZero is an interface for checking if a value is zero (for omitzero support)
type checkIsZero interface {
IsZero(ptr unsafe.Pointer) bool
}

// isZeroer is the interface implemented by types that can report whether their value is zero.
// This is compatible with Go 1.24's encoding/json omitzero feature.
type isZeroer interface {
IsZero() bool
}

func encoderOfStruct(ctx *ctx, typ reflect2.Type) ValEncoder {
type bindingTo struct {
binding *Binding
Expand Down Expand Up @@ -70,6 +82,206 @@ func createCheckIsEmpty(ctx *ctx, typ reflect2.Type) checkIsEmpty {
}
}

// isZeroerType is the reflect2.Type for the isZeroer interface
var isZeroerType = reflect2.TypeOfPtr((*isZeroer)(nil)).Elem()

// createCheckIsZero creates a zero checker for the given type.
func createCheckIsZero(ctx *ctx, typ reflect2.Type) checkIsZero {
// Check if the type implements isZeroer interface
if typ.Implements(isZeroerType) {
return &directIsZeroerChecker{valType: typ}
}
// Check if pointer to type implements isZeroer interface
ptrType := reflect2.PtrTo(typ)
if ptrType.Implements(isZeroerType) {
return &ptrIsZeroerChecker{valType: typ}
}
// Fall back to comparing with zero value
return createZeroValueChecker(typ)
}

// directIsZeroerChecker checks IsZero() for types that directly implement the interface
type directIsZeroerChecker struct {
valType reflect2.Type
}

func (checker *directIsZeroerChecker) IsZero(ptr unsafe.Pointer) bool {
obj := checker.valType.UnsafeIndirect(ptr)
if checker.valType.IsNullable() && reflect2.IsNil(obj) {
return true
}
return obj.(isZeroer).IsZero()
}

// ptrIsZeroerChecker checks IsZero() for types where *T implements the interface
type ptrIsZeroerChecker struct {
valType reflect2.Type
}

func (checker *ptrIsZeroerChecker) IsZero(ptr unsafe.Pointer) bool {
obj := checker.valType.PackEFace(ptr)
return obj.(isZeroer).IsZero()
}

// createZeroValueChecker creates a checker that compares against zero value
func createZeroValueChecker(typ reflect2.Type) checkIsZero {
kind := typ.Kind()
switch kind {
case reflect.String:
return &stringZeroChecker{}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return &intZeroChecker{typ: typ}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return &uintZeroChecker{typ: typ}
case reflect.Float32:
return &float32ZeroChecker{}
case reflect.Float64:
return &float64ZeroChecker{}
case reflect.Bool:
return &boolZeroChecker{}
case reflect.Interface, reflect.Ptr:
return &nilZeroChecker{}
case reflect.Slice, reflect.Map:
return &nilZeroChecker{}
case reflect.Array:
return createArrayZeroChecker(typ)
case reflect.Struct:
return createStructZeroChecker(typ)
default:
return nil
}
}

// stringZeroChecker checks if a string is empty
type stringZeroChecker struct{}

func (checker *stringZeroChecker) IsZero(ptr unsafe.Pointer) bool {
return *((*string)(ptr)) == ""
}

// intZeroChecker checks if an int-like value is zero
type intZeroChecker struct {
typ reflect2.Type
}

func (checker *intZeroChecker) IsZero(ptr unsafe.Pointer) bool {
switch checker.typ.Kind() {
case reflect.Int:
return *((*int)(ptr)) == 0
case reflect.Int8:
return *((*int8)(ptr)) == 0
case reflect.Int16:
return *((*int16)(ptr)) == 0
case reflect.Int32:
return *((*int32)(ptr)) == 0
case reflect.Int64:
return *((*int64)(ptr)) == 0
}
return false
}

// uintZeroChecker checks if a uint-like value is zero
type uintZeroChecker struct {
typ reflect2.Type
}

func (checker *uintZeroChecker) IsZero(ptr unsafe.Pointer) bool {
switch checker.typ.Kind() {
case reflect.Uint:
return *((*uint)(ptr)) == 0
case reflect.Uint8:
return *((*uint8)(ptr)) == 0
case reflect.Uint16:
return *((*uint16)(ptr)) == 0
case reflect.Uint32:
return *((*uint32)(ptr)) == 0
case reflect.Uint64:
return *((*uint64)(ptr)) == 0
case reflect.Uintptr:
return *((*uintptr)(ptr)) == 0
}
return false
}

// float32ZeroChecker checks if a float32 is zero
type float32ZeroChecker struct{}

func (checker *float32ZeroChecker) IsZero(ptr unsafe.Pointer) bool {
return *((*float32)(ptr)) == 0
}

// float64ZeroChecker checks if a float64 is zero
type float64ZeroChecker struct{}

func (checker *float64ZeroChecker) IsZero(ptr unsafe.Pointer) bool {
return *((*float64)(ptr)) == 0
}

// boolZeroChecker checks if a bool is false
type boolZeroChecker struct{}

func (checker *boolZeroChecker) IsZero(ptr unsafe.Pointer) bool {
return !*((*bool)(ptr))
}

// nilZeroChecker checks if a pointer/interface/slice/map is nil
type nilZeroChecker struct{}

func (checker *nilZeroChecker) IsZero(ptr unsafe.Pointer) bool {
return *((*unsafe.Pointer)(ptr)) == nil
}

// arrayBytesZeroChecker checks if all bytes in the array are zero
type arrayBytesZeroChecker struct {
size uintptr
}

func (checker *arrayBytesZeroChecker) IsZero(ptr unsafe.Pointer) bool {
bytes := (*[1 << 30]byte)(ptr)[:checker.size]
for _, b := range bytes {
if b != 0 {
return false
}
}
return true
}

func createArrayZeroChecker(typ reflect2.Type) checkIsZero {
arrayType := typ.(*reflect2.UnsafeArrayType)
elemType := arrayType.Elem()
// For simple types, we can use a simpler check
if isSimpleZeroType(elemType) {
return &arrayBytesZeroChecker{size: typ.Type1().Size()}
}
return nil // Complex arrays fall back to reflect-based comparison
}

func isSimpleZeroType(typ reflect2.Type) bool {
switch typ.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64:
return true
}
return false
}

// structZeroChecker uses reflect to check if a struct is zero
type structZeroChecker struct {
typ reflect2.Type
}

func createStructZeroChecker(typ reflect2.Type) checkIsZero {
return &structZeroChecker{typ: typ}
}

func (checker *structZeroChecker) IsZero(ptr unsafe.Pointer) bool {
// Use reflect to compare with zero value
val := checker.typ.UnsafeIndirect(ptr)
rv := reflect.ValueOf(val)
return rv.IsZero()
}

func resolveConflictBinding(cfg *frozenConfig, old, new *Binding) (ignoreOld, ignoreNew bool) {
newTagged := new.Field.Tag().Get(cfg.getTagKey()) != ""
oldTagged := old.Field.Tag().Get(cfg.getTagKey()) != ""
Expand Down Expand Up @@ -103,6 +315,8 @@ type structFieldEncoder struct {
field reflect2.StructField
fieldEncoder ValEncoder
omitempty bool
omitzero bool
zeroChecker checkIsZero
}

func (encoder *structFieldEncoder) Encode(ptr unsafe.Pointer, stream *Stream) {
Expand All @@ -118,6 +332,21 @@ func (encoder *structFieldEncoder) IsEmpty(ptr unsafe.Pointer) bool {
return encoder.fieldEncoder.IsEmpty(fieldPtr)
}

// IsZero checks if the field value is zero using the IsZero() method if available,
// or by comparing with the zero value of the type
func (encoder *structFieldEncoder) IsZero(ptr unsafe.Pointer) bool {
fieldPtr := encoder.field.UnsafeGet(ptr)
// First check if field encoder implements IsZero
if isZeroer, ok := encoder.fieldEncoder.(checkIsZero); ok {
return isZeroer.IsZero(fieldPtr)
}
// Use the pre-computed zero checker
if encoder.zeroChecker != nil {
return encoder.zeroChecker.IsZero(fieldPtr)
}
return false
}

func (encoder *structFieldEncoder) IsEmbeddedPtrNil(ptr unsafe.Pointer) bool {
isEmbeddedPtrNil, converted := encoder.fieldEncoder.(IsEmbeddedPtrNil)
if !converted {
Expand Down Expand Up @@ -148,6 +377,9 @@ func (encoder *structEncoder) Encode(ptr unsafe.Pointer, stream *Stream) {
if field.encoder.omitempty && field.encoder.IsEmpty(ptr) {
continue
}
if field.encoder.omitzero && field.encoder.IsZero(ptr) {
continue
}
if field.encoder.IsEmbeddedPtrNil(ptr) {
continue
}
Expand Down