feat: Go 1.24 omitzero struct tag support - #607
Open
bouroo wants to merge 4 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for the
omitzerostruct tag option, aligned with theomitzerosemantics
encoding/jsonintroduced in Go 1.24: a field is omitted when itsvalue is the zero value for its type unlike
omitempty, which also dropsempty-but-non-nil collections.
omitemptyomitzero0,"",false[]int{}/ mapmap[string]int{}new(int))time.Time{}(or anyIsZero() booltype)Implementation
OpStructHeadOmitZero,OpStructPtrHeadOmitZero,OpStructFieldOmitZeroroute anyomitzero-tagged field regardless ofkind (struct-end optimization is skipped for such fields).
internal/encoder/omitzero.go:IsZeroForOmitZerodispatches on the fieldkind primitives are read directly without reflection; struct and array
kinds fall back to reflect, honoring a custom
IsZero() bool(sotime.Timeand user-defined zeroers work, including marshaler-typed fields).
vm.go.tmpl, regenerated into all four VM variants) handlesboth slot layouts:
IndirectFlagsset): the slot holds a struct base; map-kindincludes store the map value (
ptrToPtr(p+offset)) asOpMaprequires;holds the field value itself maps store the value, every other kind
stores
&slot(ctxptr+code.Idx), matching the dereference contracts ofOpPtr/OpIntPtr/OpSlice.*(*T)(unsafe.Pointer(&p))double-deref idiom alreadyused throughout
internal/encoder/vm/util.go: it never converts auintptrto
unsafe.Pointerdirectly, so it passesgo vet's unsafeptr check and therace detector's checkptr validation without any suppression directives.
unchanged.
Also included: minor cleanups
min/maxrenamed tominAddr/maxAddrininternal/runtime/type.go(avoids shadowing the builtins), and staticfmt.Errorfcalls replaced witherrors.New.Testing
omitzero_test.goadds 40 subtests plus a benchmark, including regressionclasses for the tricky encoding paths:
map/slice/pointer fields (nil, empty-non-nil, non-empty)
*T) marshals: nil →null, zero struct →{}IsZero() boolimplementations whose zero disagrees withreflect.Value.IsZero, andtime.Timezero/non-zeroomitemptyvsomitzeroside-by-side and mixed-tag structsresolve #539 , closed #562