Skip to content

feat: Go 1.24 omitzero struct tag support - #607

Open
bouroo wants to merge 4 commits into
goccy:masterfrom
bouroo:feat/omitzero
Open

feat: Go 1.24 omitzero struct tag support#607
bouroo wants to merge 4 commits into
goccy:masterfrom
bouroo:feat/omitzero

Conversation

@bouroo

@bouroo bouroo commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Adds support for the omitzero struct tag option, aligned with the omitzero
semantics encoding/json introduced in Go 1.24: a field is omitted when its
value is the zero value for its type unlike omitempty, which also drops
empty-but-non-nil collections.

type T struct {
    A int       `json:"a,omitzero"` // omitted when 0
    B []int     `json:"b,omitzero"` // omitted only when nil `[]int{}` is kept
    C *int      `json:"c,omitzero"` // omitted only when nil ptr-to-0 is kept
    D time.Time `json:"d,omitzero"` // omitted when IsZero() reports zero
}
Field value omitempty omitzero
0, "", false omitted omitted
nil slice / map / pointer omitted omitted
empty non-nil slice []int{} / map map[string]int{} omitted kept
non-nil pointer to zero (new(int)) kept kept
time.Time{} (or any IsZero() bool type) kept omitted

Implementation

  • Three new opcodes OpStructHeadOmitZero, OpStructPtrHeadOmitZero,
    OpStructFieldOmitZero route any omitzero-tagged field regardless of
    kind (struct-end optimization is skipped for such fields).
  • internal/encoder/omitzero.go: IsZeroForOmitZero dispatches on the field
    kind primitives are read directly without reflection; struct and array
    kinds fall back to reflect, honoring a custom IsZero() bool (so time.Time
    and user-defined zeroers work, including marshaler-typed fields).
  • The VM template (vm.go.tmpl, regenerated into all four VM variants) handles
    both slot layouts:
    • indirect (IndirectFlags set): the slot holds a struct base; map-kind
      includes store the map value (ptrToPtr(p+offset)) as OpMap requires;
    • direct interface (pointer-shaped struct passed by value): the slot
      holds the field value itself maps store the value, every other kind
      stores &slot (ctxptr+code.Idx), matching the dereference contracts of
      OpPtr/OpIntPtr/OpSlice.
  • Unsafe access uses the *(*T)(unsafe.Pointer(&p)) double-deref idiom already
    used throughout internal/encoder/vm/util.go: it never converts a uintptr
    to unsafe.Pointer directly, so it passes go vet's unsafeptr check and the
    race detector's checkptr validation without any suppression directives.
  • Decoder side: the tag is accepted when unmarshaling; decode behavior is
    unchanged.

Also included: minor cleanups min/max renamed to minAddr/maxAddr in
internal/runtime/type.go (avoids shadowing the builtins), and static
fmt.Errorf calls replaced with errors.New.

Testing

omitzero_test.go adds 40 subtests plus a benchmark, including regression
classes for the tricky encoding paths:

  • pointer-shaped (direct-interface) structs marshaled by value single
    map/slice/pointer fields (nil, empty-non-nil, non-empty)
  • struct-pointer (*T) marshals: nil → null, zero struct → {}
  • field-level (non-head) map includes and pointer-marshal map heads
  • custom IsZero() bool implementations whose zero disagrees with
    reflect.Value.IsZero, and time.Time zero/non-zero
  • omitempty vs omitzero side-by-side and mixed-tag structs

resolve #539 , closed #562

kawinv-hash and others added 4 commits March 16, 2026 21:15
Add support for the `omitzero` struct tag that omits fields with zero values
from JSON encoding. Unlike `omitempty`, this preserves empty slices while
still omitting zero values for primitives, strings, and booleans. Includes
new opcode types (OpStructHeadOmitZero, OpStructFieldOmitZero, OpStructPtrHeadOmitZero),
runtime tag parsing, and VM handling. Also updates Go version to 1.24 and
GitHub Actions to latest versions.
- Rewrite IsZeroForOmitZero to use type switches instead of reflect for better performance
- Replace fmt.Sprintf with direct string concatenation in multiple files
- Use unsafe.Slice instead of reflect-based slice conversion in string.go
- Change fmt.Errorf to errors.New for static error messages
- Rename min/max to minAddr/maxAddr in runtime/type.go for clarity

These changes improve performance by reducing reflection overhead and unnecessary allocations.
The VM treated direct interface slots as pointers to the field,
so zero checks dereferenced the wrong address, and map fields
were passed by address instead of by value. Distinguish indirect
slots (which hold the struct base) from direct ones and
dereference map values before storing.

Also rewrite IsZeroForOmitZero with the double-deref unsafe
idiom so it passes go vet's unsafeptr check and the race
detector's checkptr validation, dropping go:nocheckptr.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

omitzero support

2 participants