From e5d3c84c40dd483921dbfcd3c404e702541c6b60 Mon Sep 17 00:00:00 2001 From: Muhammad Khuzaima Umair Date: Wed, 26 Nov 2025 15:42:37 +0500 Subject: [PATCH] add support for `omitzero` --- README.md | 17 +++ reflect_extension.go | 23 +++- reflect_struct_encoder.go | 234 +++++++++++++++++++++++++++++++++++++- 3 files changed, 268 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c589addf..b664980f 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/reflect_extension.go b/reflect_extension.go index 74a97bfe..a7c763e3 100644 --- a/reflect_extension.go +++ b/reflect_extension.go @@ -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) } @@ -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) @@ -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} @@ -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} } } diff --git a/reflect_struct_encoder.go b/reflect_struct_encoder.go index 152e3ef5..e471b2fd 100644 --- a/reflect_struct_encoder.go +++ b/reflect_struct_encoder.go @@ -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 @@ -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()) != "" @@ -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) { @@ -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 { @@ -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 }