Skip to content

Commit ab3ac2c

Browse files
committed
feat: PR mailru#429 (onitzero)
1 parent 792f1af commit ab3ac2c

9 files changed

Lines changed: 214 additions & 6 deletions

File tree

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ generate: build
1717
./tests/snake.go \
1818
./tests/data.go \
1919
./tests/omitempty.go \
20+
./tests/omitzero.go \
2021
./tests/nothing.go \
2122
./tests/named_type.go \
2223
./tests/custom_map_key_type.go \
@@ -55,6 +56,7 @@ generate: build
5556
./tests/text_marshaler.go
5657
bin/easyjson -snake_case ./tests/snake.go
5758
bin/easyjson -omit_empty ./tests/omitempty.go
59+
bin/easyjson -omit_zero ./tests/omitzero.go
5860
bin/easyjson -build_tags=use_easyjson -disable_members_unescape ./benchmark/data.go
5961
bin/easyjson -disallow_unknown_fields ./tests/disallow_unknown.go
6062
bin/easyjson -disable_members_unescape ./tests/members_unescaped.go

bootstrap/bootstrap.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Generator struct {
3030
SnakeCase bool
3131
LowerCamelCase bool
3232
OmitEmpty bool
33+
OmitZero bool
3334
DisallowUnknownFields bool
3435
SkipMemberNameUnescaping bool
3536

@@ -94,7 +95,7 @@ func (g *Generator) writeMain() (path string, err error) {
9495

9596
fmt.Fprintln(f, "// +build ignore")
9697
fmt.Fprintln(f)
97-
fmt.Fprintln(f, "// TEMPORARY AUTOGENERATED FILE: easyjson bootstapping code to launch")
98+
fmt.Fprintln(f, "// TEMPORARY AUTOGENERATED FILE: easyjson bootstrapping code to launch")
9899
fmt.Fprintln(f, "// the actual generator.")
99100
fmt.Fprintln(f)
100101
fmt.Fprintln(f, "package main")
@@ -125,6 +126,9 @@ func (g *Generator) writeMain() (path string, err error) {
125126
if g.OmitEmpty {
126127
fmt.Fprintln(f, " g.OmitEmpty()")
127128
}
129+
if g.OmitZero {
130+
fmt.Fprintln(f, " g.OmitZero()")
131+
}
128132
if g.NoStdMarshalers {
129133
fmt.Fprintln(f, " g.NoStdMarshalers()")
130134
}

easyjson/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var snakeCase = flag.Bool("snake_case", false, "use snake_case names instead of
2727
var lowerCamelCase = flag.Bool("lower_camel_case", false, "use lowerCamelCase names instead of CamelCase by default")
2828
var noStdMarshalers = flag.Bool("no_std_marshalers", false, "don't generate MarshalJSON/UnmarshalJSON funcs")
2929
var omitEmpty = flag.Bool("omit_empty", false, "omit empty fields by default")
30+
var omitZero = flag.Bool("omit_zero", false, "omit zero value fields by default")
3031
var allStructs = flag.Bool("all", false, "generate marshaler/unmarshalers for all structs in a file")
3132
var simpleBytes = flag.Bool("byte", false, "use simple bytes instead of Base64Bytes for slice of bytes")
3233
var leaveTemps = flag.Bool("leave_temps", false, "do not delete temporary files")
@@ -86,6 +87,7 @@ func generate(fname string) (err error) {
8687
DisallowUnknownFields: *disallowUnknownFields,
8788
SkipMemberNameUnescaping: *skipMemberNameUnescaping,
8889
OmitEmpty: *omitEmpty,
90+
OmitZero: *omitZero,
8991
LeaveTemps: *leaveTemps,
9092
OutName: outName,
9193
StubsOnly: *stubs,

gen/encoder.go

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ type fieldTags struct {
5656
omit bool
5757
omitEmpty bool
5858
noOmitEmpty bool
59+
omitZero bool
60+
noOmitZero bool
5961
asString bool
6062
required bool
6163
intern bool
@@ -76,6 +78,10 @@ func parseFieldTags(f reflect.StructField) fieldTags {
7678
ret.omitEmpty = true
7779
case s == "!omitempty":
7880
ret.noOmitEmpty = true
81+
case s == "omitzero":
82+
ret.omitZero = true
83+
case s == "!omitzero":
84+
ret.noOmitZero = true
7985
case s == "string":
8086
ret.asString = true
8187
case s == "required":
@@ -330,7 +336,8 @@ func (g *Generator) notEmptyCheck(t reflect.Type, v string) string {
330336
return v + ` != ""`
331337
case reflect.Float32, reflect.Float64,
332338
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
333-
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
339+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
340+
reflect.Uintptr:
334341

335342
return v + " != 0"
336343

@@ -340,6 +347,78 @@ func (g *Generator) notEmptyCheck(t reflect.Type, v string) string {
340347
}
341348
}
342349

350+
func (g *Generator) notZeroCheck(t reflect.Type, v string) string {
351+
optionalIface := reflect.TypeOf((*easyjson.IsZero)(nil)).Elem()
352+
if reflect.PtrTo(t).Implements(optionalIface) {
353+
return "!(" + v + ").IsZero()"
354+
}
355+
356+
switch t.Kind() {
357+
case reflect.Slice, reflect.Map, reflect.Interface, reflect.Ptr:
358+
return v + " != nil"
359+
case reflect.Bool:
360+
return v
361+
case reflect.String:
362+
return v + ` != ""`
363+
case reflect.Float32, reflect.Float64,
364+
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
365+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
366+
reflect.Uintptr:
367+
368+
return v + " != 0"
369+
case reflect.Array:
370+
// NOTE: stdlib encoding/json does not check if array elements implement IsZero, so we don't either
371+
return "(" + v + " != " + g.getType(t) + "{})"
372+
case reflect.Struct:
373+
// NOTE: stdlib encoding/json does not check if struct fields implement IsZero, so we don't either
374+
return "(" + v + " != " + g.getType(t) + "{})"
375+
376+
default:
377+
return "true"
378+
}
379+
}
380+
381+
func (g *Generator) notEmptyOrZeroCheck(t reflect.Type, v string) string {
382+
isDefinedIface := reflect.TypeOf((*easyjson.Optional)(nil)).Elem()
383+
implementsIsDefined := reflect.PtrTo(t).Implements(isDefinedIface)
384+
isZeroIface := reflect.TypeOf((*easyjson.IsZero)(nil)).Elem()
385+
implementsIsZero := reflect.PtrTo(t).Implements(isZeroIface)
386+
387+
if implementsIsDefined && implementsIsZero {
388+
return "(" + v + ").IsDefined() || !(" + v + ").IsZero()"
389+
} else if implementsIsDefined {
390+
return "(" + v + ").IsDefined()"
391+
} else if implementsIsZero {
392+
return "!(" + v + ").IsZero()"
393+
}
394+
395+
switch t.Kind() {
396+
case reflect.Slice, reflect.Map:
397+
return v + " != nil && len(" + v + ") != 0"
398+
case reflect.Interface, reflect.Ptr:
399+
return v + " != nil"
400+
case reflect.Bool:
401+
return v
402+
case reflect.String:
403+
return v + ` != ""`
404+
case reflect.Float32, reflect.Float64,
405+
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
406+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
407+
reflect.Uintptr:
408+
409+
return v + " != 0"
410+
case reflect.Array:
411+
// NOTE: stdlib encoding/json does not check if array elements implement IsZero, so we don't either
412+
return "(" + v + " != " + g.getType(t) + "{})"
413+
case reflect.Struct:
414+
// NOTE: stdlib encoding/json does not check if struct fields implement IsZero, so we don't either
415+
return "(" + v + " != " + g.getType(t) + "{})"
416+
417+
default:
418+
return "true"
419+
}
420+
}
421+
343422
func (g *Generator) genStructFieldEncoder(t reflect.Type, f reflect.StructField, first, firstCondition bool) (bool, error) {
344423
jsonName := g.fieldNamer.GetJSONFieldName(t, f)
345424
tags := parseFieldTags(f)
@@ -351,18 +430,25 @@ func (g *Generator) genStructFieldEncoder(t reflect.Type, f reflect.StructField,
351430
toggleFirstCondition := firstCondition
352431

353432
noOmitEmpty := (!tags.omitEmpty && !g.omitEmpty) || tags.noOmitEmpty
354-
if noOmitEmpty {
433+
noOmitZero := (!tags.omitZero && !g.omitZero) || tags.noOmitZero
434+
if noOmitEmpty && noOmitZero {
355435
fmt.Fprintln(g.out, " {")
356436
toggleFirstCondition = false
357-
} else {
437+
} else if noOmitZero {
358438
fmt.Fprintln(g.out, " if", g.notEmptyCheck(f.Type, "in."+f.Name), "{")
359439
// can be any in runtime, so toggleFirstCondition stay as is
440+
} else if noOmitEmpty {
441+
fmt.Fprintln(g.out, " if", g.notZeroCheck(f.Type, "in."+f.Name), "{")
442+
// can be any in runtime, so toggleFirstCondition stay as is
443+
} else {
444+
fmt.Fprintln(g.out, " if", g.notEmptyOrZeroCheck(f.Type, "in."+f.Name), "{")
445+
// can be any in runtime, so toggleFirstCondition stay as is
360446
}
361447

362448
if firstCondition {
363449
fmt.Fprintf(g.out, " const prefix string = %q\n", ","+strconv.Quote(jsonName)+":")
364450
if first {
365-
if !noOmitEmpty {
451+
if !noOmitEmpty || !noOmitZero {
366452
fmt.Fprintln(g.out, " first = false")
367453
}
368454
fmt.Fprintln(g.out, " out.RawString(prefix[1:])")

gen/generator.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Generator struct {
3535

3636
noStdMarshalers bool
3737
omitEmpty bool
38+
omitZero bool
3839
disallowUnknownFields bool
3940
fieldNamer FieldNamer
4041
simpleBytes bool
@@ -133,6 +134,11 @@ func (g *Generator) SimpleBytes() {
133134
g.simpleBytes = true
134135
}
135136

137+
// OmitZero triggers `json=",omitzero"` behaviour by default.
138+
func (g *Generator) OmitZero() {
139+
g.omitZero = true
140+
}
141+
136142
// addTypes requests to generate encoding/decoding funcs for the given type.
137143
func (g *Generator) addType(t reflect.Type) {
138144
if g.typesSeen[t] {

helpers.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ type Optional interface {
3232
IsDefined() bool
3333
}
3434

35+
// IsZero defines a zero-test method for a type to integrate with 'omitzero' logic.
36+
type IsZero interface {
37+
IsZero() bool
38+
}
39+
3540
// UnknownsUnmarshaler provides a method to unmarshal unknown struct fileds and save them as you want
3641
type UnknownsUnmarshaler interface {
3742
UnmarshalUnknown(in *jlexer.Lexer, key string)

tests/basic_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ var testCases = []struct {
2424
{&namedPrimitiveTypesValue, namedPrimitiveTypesString},
2525
{&structsValue, structsString},
2626
{&omitEmptyValue, omitEmptyString},
27+
{&omitZeroValue, omitZeroString},
28+
{&omitEmptyAndZeroValue, omitEmptyAndZeroString},
2729
{&snakeStructValue, snakeStructString},
2830
{&omitEmptyDefaultValue, omitEmptyDefaultString},
31+
{&omitZeroDefaultValue, omitZeroDefaultString},
2932
{&optsValue, optsString},
3033
{&rawValue, rawString},
3134
{&stdMarshalerValue, stdMarshalerString},
@@ -243,7 +246,7 @@ func TestNestedMarshaler(t *testing.T) {
243246
t.Errorf("Can't marshal NestedMarshaler: %s", err)
244247
}
245248

246-
s2 := NestedMarshaler {
249+
s2 := NestedMarshaler{
247250
Value: &StructWithMarshaler{},
248251
}
249252

tests/data.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,80 @@ var omitEmptyString = "{" +
358358
`"SubPNE":{"Value":"3","Value2":"4"}` +
359359
"}"
360360

361+
type OmitZero struct {
362+
// NOTE: first field is empty to test comma printing.
363+
364+
StrZ, StrNZ string `json:",omitzero"`
365+
ZPtr, PtrZ, PtrNZ *string `json:",omitzero"`
366+
367+
IntNZ int `json:"intField,omitzero"`
368+
IntZ int `json:",omitzero"`
369+
370+
// NOTE: omitzero DOES have effect on non-pointer struct fields.
371+
SubZ, SubNZ SubStruct `json:",omitzero"`
372+
SubZP, SubPZ, SubPNZ *SubStruct `json:",omitzero"`
373+
374+
// test IsZero()bool is respected
375+
Time time.Time `json:",omitzero"`
376+
}
377+
378+
var omitZeroValue = OmitZero{
379+
StrNZ: "str",
380+
PtrZ: new(string),
381+
PtrNZ: &str,
382+
IntNZ: 6,
383+
SubNZ: SubStruct{Value: "1", Value2: "2"},
384+
SubPZ: &SubStruct{},
385+
SubPNZ: &SubStruct{Value: "3", Value2: "4"},
386+
}
387+
388+
var omitZeroString = "{" +
389+
`"StrNZ":"str",` +
390+
`"PtrZ":"",` +
391+
`"PtrNZ":"bla",` +
392+
`"intField":6,` +
393+
`"SubNZ":{"Value":"1","Value2":"2"},` +
394+
`"SubPZ":{"Value":"","Value2":""},` +
395+
`"SubPNZ":{"Value":"3","Value2":"4"}` +
396+
"}"
397+
398+
type OmitEmptyAndZero struct {
399+
// NOTE: first field is empty to test comma printing.
400+
401+
StrZ, StrNZ string `json:",omitempty,omitzero"`
402+
ZPtr, PtrZ, PtrNZ *string `json:",omitempty,omitzero"`
403+
404+
IntNZ int `json:"intField,omitempty,omitzero"`
405+
IntZ int `json:",omitempty,omitzero"`
406+
407+
// NOTE: omitzero DOES have effect on non-pointer struct fields.
408+
SubZ, SubNZ SubStruct `json:",omitempty,omitzero"`
409+
SubZP, SubPZ, SubPNZ *SubStruct `json:",omitempty,omitzero"`
410+
411+
// test IsZero()bool is respected
412+
Time time.Time `json:",omitempty,omitzero"`
413+
}
414+
415+
var omitEmptyAndZeroValue = OmitZero{
416+
StrNZ: "str",
417+
PtrZ: new(string),
418+
PtrNZ: &str,
419+
IntNZ: 6,
420+
SubNZ: SubStruct{Value: "1", Value2: "2"},
421+
SubPZ: &SubStruct{},
422+
SubPNZ: &SubStruct{Value: "3", Value2: "4"},
423+
}
424+
425+
var omitEmptyAndZeroString = "{" +
426+
`"StrNZ":"str",` +
427+
`"PtrZ":"",` +
428+
`"PtrNZ":"bla",` +
429+
`"intField":6,` +
430+
`"SubNZ":{"Value":"1","Value2":"2"},` +
431+
`"SubPZ":{"Value":"","Value2":""},` +
432+
`"SubPNZ":{"Value":"3","Value2":"4"}` +
433+
"}"
434+
361435
type Opts struct {
362436
StrNull opt.String
363437
StrEmpty opt.String

tests/omitzero.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package tests
2+
3+
import "time"
4+
5+
//easyjson:json
6+
type OmitZeroDefault struct {
7+
Field string
8+
Str string
9+
Str1 string `json:"s,!omitzero"`
10+
Str2 string `json:",!omitzero"`
11+
Time time.Time `json:",omitzero"` // implements IsZero() bool
12+
Array1 [2]int `json:",omitzero"` // array filled with zero values is omitted
13+
Array2 [2]int `json:",omitzero"` // array filled with non-zero values is outputed
14+
Array3 [2]OmitZeroSubstruct `json:",omitzero"`
15+
Array4 [2]OmitZeroSubstruct `json:",omitzero"`
16+
Struct1 OmitZeroSubstruct `json:",omitzero"`
17+
Struct2 OmitZeroSubstruct `json:",!omitzero"` // struct where all the fields are tagged omitzero
18+
}
19+
20+
var omitZeroDefaultValue = OmitZeroDefault{Field: "test", Array2: [2]int{0, 1}, Array4: [2]OmitZeroSubstruct{{}, {F: 1}}}
21+
var omitZeroDefaultString = `{"Field":"test","s":"","Str2":"","Array2":[0,1],"Array4":[{},{"F":1}],"Struct2":{}}`
22+
23+
type OmitZeroSubstruct struct {
24+
F float32 `json:",omitzero"`
25+
T time.Time `json:",omitzero"`
26+
}

0 commit comments

Comments
 (0)