Support for json:",omitzero" tag (#429)

* add support for the json:",omitzero" tag

omitzero was added to the go stdlib encoder/json in go 1.24

* add case which omitempty missed: uintptr is encodable in json

and does accept omitempty in the go stdlib encoder/json.

* add omitzero test cases for pointer-to-"" and pointer-to-{}

(both should marshal to something in order to match stdlib json, and
they do)

* fix and add tests for the case when omitzero is combined with omitempty

Without a special case the generated code can look like

   if v != nil && v != nil {
or
   if v != "" && v != "" {

and that gets flagged by go vet (go 1.25.5)
This commit is contained in:
Nicolas S. Dade
2026-03-14 17:51:56 +03:00
committed by GitHub
parent 7dcf6870f0
commit 89250dbfdd
9 changed files with 212 additions and 4 deletions
+2
View File
@@ -17,6 +17,7 @@ generate: build
./tests/snake.go \
./tests/data.go \
./tests/omitempty.go \
./tests/omitzero.go \
./tests/nothing.go \
./tests/named_type.go \
./tests/custom_map_key_type.go \
@@ -54,6 +55,7 @@ generate: build
./tests/text_marshaler.go
bin/easyjson -snake_case ./tests/snake.go
bin/easyjson -omit_empty ./tests/omitempty.go
bin/easyjson -omit_zero ./tests/omitzero.go
bin/easyjson -build_tags=use_easyjson -disable_members_unescape ./benchmark/data.go
bin/easyjson -disallow_unknown_fields ./tests/disallow_unknown.go
bin/easyjson -disable_members_unescape ./tests/members_unescaped.go
+4
View File
@@ -30,6 +30,7 @@ type Generator struct {
SnakeCase bool
LowerCamelCase bool
OmitEmpty bool
OmitZero bool
DisallowUnknownFields bool
SkipMemberNameUnescaping bool
@@ -125,6 +126,9 @@ func (g *Generator) writeMain() (path string, err error) {
if g.OmitEmpty {
fmt.Fprintln(f, " g.OmitEmpty()")
}
if g.OmitZero {
fmt.Fprintln(f, " g.OmitZero()")
}
if g.NoStdMarshalers {
fmt.Fprintln(f, " g.NoStdMarshalers()")
}
+2
View File
@@ -27,6 +27,7 @@ var snakeCase = flag.Bool("snake_case", false, "use snake_case names instead of
var lowerCamelCase = flag.Bool("lower_camel_case", false, "use lowerCamelCase names instead of CamelCase by default")
var noStdMarshalers = flag.Bool("no_std_marshalers", false, "don't generate MarshalJSON/UnmarshalJSON funcs")
var omitEmpty = flag.Bool("omit_empty", false, "omit empty fields by default")
var omitZero = flag.Bool("omit_zero", false, "omit zero value fields by default")
var allStructs = flag.Bool("all", false, "generate marshaler/unmarshalers for all structs in a file")
var simpleBytes = flag.Bool("byte", false, "use simple bytes instead of Base64Bytes for slice of bytes")
var leaveTemps = flag.Bool("leave_temps", false, "do not delete temporary files")
@@ -86,6 +87,7 @@ func generate(fname string) (err error) {
DisallowUnknownFields: *disallowUnknownFields,
SkipMemberNameUnescaping: *skipMemberNameUnescaping,
OmitEmpty: *omitEmpty,
OmitZero: *omitZero,
LeaveTemps: *leaveTemps,
OutName: outName,
StubsOnly: *stubs,
+90 -4
View File
@@ -56,6 +56,8 @@ type fieldTags struct {
omit bool
omitEmpty bool
noOmitEmpty bool
omitZero bool
noOmitZero bool
asString bool
required bool
intern bool
@@ -76,6 +78,10 @@ func parseFieldTags(f reflect.StructField) fieldTags {
ret.omitEmpty = true
case s == "!omitempty":
ret.noOmitEmpty = true
case s == "omitzero":
ret.omitZero = true
case s == "!omitzero":
ret.noOmitZero = true
case s == "string":
ret.asString = true
case s == "required":
@@ -314,7 +320,8 @@ func (g *Generator) notEmptyCheck(t reflect.Type, v string) string {
return v + ` != ""`
case reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr:
return v + " != 0"
@@ -324,6 +331,78 @@ func (g *Generator) notEmptyCheck(t reflect.Type, v string) string {
}
}
func (g *Generator) notZeroCheck(t reflect.Type, v string) string {
optionalIface := reflect.TypeOf((*easyjson.IsZero)(nil)).Elem()
if reflect.PtrTo(t).Implements(optionalIface) {
return "!(" + v + ").IsZero()"
}
switch t.Kind() {
case reflect.Slice, reflect.Map, reflect.Interface, reflect.Ptr:
return v + " != nil"
case reflect.Bool:
return v
case reflect.String:
return v + ` != ""`
case reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr:
return v + " != 0"
case reflect.Array:
// NOTE: stdlib encoding/json does not check if array elements implement IsZero, so we don't either
return "(" + v + " != " + g.getType(t) + "{})"
case reflect.Struct:
// NOTE: stdlib encoding/json does not check if struct fields implement IsZero, so we don't either
return "(" + v + " != " + g.getType(t) + "{})"
default:
return "true"
}
}
func (g *Generator) notEmptyOrZeroCheck(t reflect.Type, v string) string {
isDefinedIface := reflect.TypeOf((*easyjson.Optional)(nil)).Elem()
implementsIsDefined := reflect.PtrTo(t).Implements(isDefinedIface)
isZeroIface := reflect.TypeOf((*easyjson.IsZero)(nil)).Elem()
implementsIsZero := reflect.PtrTo(t).Implements(isZeroIface)
if implementsIsDefined && implementsIsZero {
return "(" + v + ").IsDefined() || !(" + v + ").IsZero()"
} else if implementsIsDefined {
return "(" + v + ").IsDefined()"
} else if implementsIsZero {
return "!(" + v + ").IsZero()"
}
switch t.Kind() {
case reflect.Slice, reflect.Map:
return v + " != nil && len(" + v + ") != 0"
case reflect.Interface, reflect.Ptr:
return v + " != nil"
case reflect.Bool:
return v
case reflect.String:
return v + ` != ""`
case reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Uintptr:
return v + " != 0"
case reflect.Array:
// NOTE: stdlib encoding/json does not check if array elements implement IsZero, so we don't either
return "(" + v + " != " + g.getType(t) + "{})"
case reflect.Struct:
// NOTE: stdlib encoding/json does not check if struct fields implement IsZero, so we don't either
return "(" + v + " != " + g.getType(t) + "{})"
default:
return "true"
}
}
func (g *Generator) genStructFieldEncoder(t reflect.Type, f reflect.StructField, first, firstCondition bool) (bool, error) {
jsonName := g.fieldNamer.GetJSONFieldName(t, f)
tags := parseFieldTags(f)
@@ -335,18 +414,25 @@ func (g *Generator) genStructFieldEncoder(t reflect.Type, f reflect.StructField,
toggleFirstCondition := firstCondition
noOmitEmpty := (!tags.omitEmpty && !g.omitEmpty) || tags.noOmitEmpty
if noOmitEmpty {
noOmitZero := (!tags.omitZero && !g.omitZero) || tags.noOmitZero
if noOmitEmpty && noOmitZero {
fmt.Fprintln(g.out, " {")
toggleFirstCondition = false
} else {
} else if noOmitZero {
fmt.Fprintln(g.out, " if", g.notEmptyCheck(f.Type, "in."+f.Name), "{")
// can be any in runtime, so toggleFirstCondition stay as is
} else if noOmitEmpty {
fmt.Fprintln(g.out, " if", g.notZeroCheck(f.Type, "in."+f.Name), "{")
// can be any in runtime, so toggleFirstCondition stay as is
} else {
fmt.Fprintln(g.out, " if", g.notEmptyOrZeroCheck(f.Type, "in."+f.Name), "{")
// can be any in runtime, so toggleFirstCondition stay as is
}
if firstCondition {
fmt.Fprintf(g.out, " const prefix string = %q\n", ","+strconv.Quote(jsonName)+":")
if first {
if !noOmitEmpty {
if !noOmitEmpty || !noOmitZero {
fmt.Fprintln(g.out, " first = false")
}
fmt.Fprintln(g.out, " out.RawString(prefix[1:])")
+6
View File
@@ -35,6 +35,7 @@ type Generator struct {
noStdMarshalers bool
omitEmpty bool
omitZero bool
disallowUnknownFields bool
fieldNamer FieldNamer
simpleBytes bool
@@ -128,6 +129,11 @@ func (g *Generator) OmitEmpty() {
g.omitEmpty = true
}
// OmitZero triggers `json=",omitzero"` behaviour by default.
func (g *Generator) OmitZero() {
g.omitZero = true
}
// SimpleBytes triggers generate output bytes as slice byte
func (g *Generator) SimpleBytes() {
g.simpleBytes = true
+5
View File
@@ -33,6 +33,11 @@ type Optional interface {
IsDefined() bool
}
// IsZero defines a zero-test method for a type to integrate with 'omitzero' logic.
type IsZero interface {
IsZero() bool
}
// UnknownsUnmarshaler provides a method to unmarshal unknown struct fileds and save them as you want
type UnknownsUnmarshaler interface {
UnmarshalUnknown(in *jlexer.Lexer, key string)
+3
View File
@@ -24,8 +24,11 @@ var testCases = []struct {
{&namedPrimitiveTypesValue, namedPrimitiveTypesString},
{&structsValue, structsString},
{&omitEmptyValue, omitEmptyString},
{&omitZeroValue, omitZeroString},
{&omitEmptyAndZeroValue, omitEmptyAndZeroString},
{&snakeStructValue, snakeStructString},
{&omitEmptyDefaultValue, omitEmptyDefaultString},
{&omitZeroDefaultValue, omitZeroDefaultString},
{&optsValue, optsString},
{&rawValue, rawString},
{&stdMarshalerValue, stdMarshalerString},
+74
View File
@@ -358,6 +358,80 @@ var omitEmptyString = "{" +
`"SubPNE":{"Value":"3","Value2":"4"}` +
"}"
type OmitZero struct {
// NOTE: first field is empty to test comma printing.
StrZ, StrNZ string `json:",omitzero"`
ZPtr, PtrZ, PtrNZ *string `json:",omitzero"`
IntNZ int `json:"intField,omitzero"`
IntZ int `json:",omitzero"`
// NOTE: omitzero DOES have effect on non-pointer struct fields.
SubZ, SubNZ SubStruct `json:",omitzero"`
SubZP, SubPZ, SubPNZ *SubStruct `json:",omitzero"`
// test IsZero()bool is respected
Time time.Time `json:",omitzero"`
}
var omitZeroValue = OmitZero{
StrNZ: "str",
PtrZ: new(string),
PtrNZ: &str,
IntNZ: 6,
SubNZ: SubStruct{Value: "1", Value2: "2"},
SubPZ: &SubStruct{},
SubPNZ: &SubStruct{Value: "3", Value2: "4"},
}
var omitZeroString = "{" +
`"StrNZ":"str",` +
`"PtrZ":"",` +
`"PtrNZ":"bla",` +
`"intField":6,` +
`"SubNZ":{"Value":"1","Value2":"2"},` +
`"SubPZ":{"Value":"","Value2":""},` +
`"SubPNZ":{"Value":"3","Value2":"4"}` +
"}"
type OmitEmptyAndZero struct {
// NOTE: first field is empty to test comma printing.
StrZ, StrNZ string `json:",omitempty,omitzero"`
ZPtr, PtrZ, PtrNZ *string `json:",omitempty,omitzero"`
IntNZ int `json:"intField,omitempty,omitzero"`
IntZ int `json:",omitempty,omitzero"`
// NOTE: omitzero DOES have effect on non-pointer struct fields.
SubZ, SubNZ SubStruct `json:",omitempty,omitzero"`
SubZP, SubPZ, SubPNZ *SubStruct `json:",omitempty,omitzero"`
// test IsZero()bool is respected
Time time.Time `json:",omitempty,omitzero"`
}
var omitEmptyAndZeroValue = OmitZero{
StrNZ: "str",
PtrZ: new(string),
PtrNZ: &str,
IntNZ: 6,
SubNZ: SubStruct{Value: "1", Value2: "2"},
SubPZ: &SubStruct{},
SubPNZ: &SubStruct{Value: "3", Value2: "4"},
}
var omitEmptyAndZeroString = "{" +
`"StrNZ":"str",` +
`"PtrZ":"",` +
`"PtrNZ":"bla",` +
`"intField":6,` +
`"SubNZ":{"Value":"1","Value2":"2"},` +
`"SubPZ":{"Value":"","Value2":""},` +
`"SubPNZ":{"Value":"3","Value2":"4"}` +
"}"
type Opts struct {
StrNull opt.String
StrEmpty opt.String
+26
View File
@@ -0,0 +1,26 @@
package tests
import "time"
//easyjson:json
type OmitZeroDefault struct {
Field string
Str string
Str1 string `json:"s,!omitzero"`
Str2 string `json:",!omitzero"`
Time time.Time `json:",omitzero"` // implements IsZero() bool
Array1 [2]int `json:",omitzero"` // array filled with zero values is omitted
Array2 [2]int `json:",omitzero"` // array filled with non-zero values is outputed
Array3 [2]OmitZeroSubstruct `json:",omitzero"`
Array4 [2]OmitZeroSubstruct `json:",omitzero"`
Struct1 OmitZeroSubstruct `json:",omitzero"`
Struct2 OmitZeroSubstruct `json:",!omitzero"` // struct where all the fields are tagged omitzero
}
var omitZeroDefaultValue = OmitZeroDefault{Field: "test", Array2: [2]int{0, 1}, Array4: [2]OmitZeroSubstruct{{}, {F: 1}}}
var omitZeroDefaultString = `{"Field":"test","s":"","Str2":"","Array2":[0,1],"Array4":[{},{"F":1}],"Struct2":{}}`
type OmitZeroSubstruct struct {
F float32 `json:",omitzero"`
T time.Time `json:",omitzero"`
}