diff --git a/Makefile b/Makefile index ee7f689..eed947e 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index aff4fb4..733123d 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -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()") } diff --git a/easyjson/main.go b/easyjson/main.go index 55be0ac..9283592 100644 --- a/easyjson/main.go +++ b/easyjson/main.go @@ -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, diff --git a/gen/encoder.go b/gen/encoder.go index ed6d6ad..853d983 100644 --- a/gen/encoder.go +++ b/gen/encoder.go @@ -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,37 @@ 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) genStructFieldEncoder(t reflect.Type, f reflect.StructField, first, firstCondition bool) (bool, error) { jsonName := g.fieldNamer.GetJSONFieldName(t, f) tags := parseFieldTags(f) @@ -335,18 +373,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.notEmptyCheck(f.Type, "in."+f.Name), "&&", g.notZeroCheck(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:])") diff --git a/gen/generator.go b/gen/generator.go index 8598fe0..32fa79f 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -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 diff --git a/helpers.go b/helpers.go index efe34bf..6a4b2fd 100644 --- a/helpers.go +++ b/helpers.go @@ -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) diff --git a/tests/basic_test.go b/tests/basic_test.go index 5bf93f1..b7f2ab1 100644 --- a/tests/basic_test.go +++ b/tests/basic_test.go @@ -24,8 +24,10 @@ var testCases = []struct { {&namedPrimitiveTypesValue, namedPrimitiveTypesString}, {&structsValue, structsString}, {&omitEmptyValue, omitEmptyString}, + {&omitZeroValue, omitZeroString}, {&snakeStructValue, snakeStructString}, {&omitEmptyDefaultValue, omitEmptyDefaultString}, + {&omitZeroDefaultValue, omitZeroDefaultString}, {&optsValue, optsString}, {&rawValue, rawString}, {&stdMarshalerValue, stdMarshalerString}, diff --git a/tests/data.go b/tests/data.go index 2018c88..e08396e 100644 --- a/tests/data.go +++ b/tests/data.go @@ -358,6 +358,39 @@ var omitEmptyString = "{" + `"SubPNE":{"Value":"3","Value2":"4"}` + "}" +type OmitZero struct { + // NOTE: first field is empty to test comma printing. + + StrZ, StrNZ string `json:",omitzero"` + 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"` + SubPZ, SubPNZ *SubStruct `json:",omitzero"` + + // test IsZero()bool is repected + Time time.Time `json:",omitzero"` +} + +var omitZeroValue = OmitZero{ + StrNZ: "str", + PtrNZ: &str, + IntNZ: 6, + SubNZ: SubStruct{Value: "1", Value2: "2"}, + SubPNZ: &SubStruct{Value: "3", Value2: "4"}, +} + +var omitZeroString = "{" + + `"StrNZ":"str",` + + `"PtrNZ":"bla",` + + `"intField":6,` + + `"SubNZ":{"Value":"1","Value2":"2"},` + + `"SubPNZ":{"Value":"3","Value2":"4"}` + + "}" + type Opts struct { StrNull opt.String StrEmpty opt.String diff --git a/tests/omitzero.go b/tests/omitzero.go new file mode 100644 index 0000000..91c62c4 --- /dev/null +++ b/tests/omitzero.go @@ -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"` +}