Skip to content

Commit 7b4b7e7

Browse files
authored
Merge pull request #11 from dumim/develop
Omitempty implemented with intelligent default value checks
2 parents dc779c1 + 00b6a39 commit 7b4b7e7

File tree

3 files changed

+117
-2
lines changed

3 files changed

+117
-2
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ whereas using `bar` (`ToMap(obj, "bar")`) on the same `obj` will result in:
118118
- Dot notation will create a parent-child relationship for every `.`.
119119
- Not setting any tag will ignore that field, unless if it's a struct; then it will go inside the struct to check its tags
120120
- `-` will explicitly ignore that field. As opposed to above, it will not look inside even if the field is of struct type.
121+
- `,omitempty` can be used similar to the `json` package to skip fields that have empty values
122+
- this also handles default empty values (`""` for `string`, `0` for `int`, etc.). Note that for `bool` values, `false` wil be omitted if this option is used since this is the default empty value. To work around this, use `*bool` (see example)
121123

122124
For an example that includes all the above scenarios see the code below:
123125

@@ -139,6 +141,23 @@ type ObjThree struct {
139141
Name string `custom:"name"`
140142
Value int `custom:"value"`
141143
}
144+
type ObjFour struct {
145+
F1 string `custom:"f1,omitempty"`
146+
F2 struct {
147+
F21 string `custom:"f21,omitempty"`
148+
} `custom:"f2"`
149+
F3 *string `custom:"f3, omitempty"` // omitempty with space
150+
F4 int `custom:"f4,omitempty"`
151+
F5 bool `custom:"f5,omitempty"`
152+
F6 interface{} `custom:"f6,omitempty"`
153+
F7 struct {
154+
F71 string `custom:"f71"`
155+
} `custom:"f7,omitempty"`
156+
F8 *bool `custom:"f8,omitempty"` // use pointer to keep false on omitempty
157+
F9 struct {
158+
F91 string `custom:"f91"`
159+
} `custom:"f9,omitempty"`
160+
}
142161
type Example struct {
143162
Name string `custom:"name"`
144163
Email string `custom:"email"`
@@ -148,10 +167,12 @@ type Example struct {
148167
Id int `custom:"id"`
149168
Call int `custom:"data.call"` // top-level dot notation
150169
ArrayObj []ObjThree `custom:"list"`
170+
Omit ObjFour `custom:omit`
151171
}
152172
```
153173
The `ToMap` function can be used to convert this into a JSON/Map based on the values defined in the given custom tag like so.
154174
```go
175+
f := false
155176
obj := Example{
156177
Name: "2",
157178
Email: "3",
@@ -170,7 +191,12 @@ obj := Example{
170191
{"hi", 1},
171192
{"world", 2},
172193
},
194+
Omit: ObjFour{
195+
F5: f,
196+
F8: &f,
197+
}
173198
}
199+
obj.Omit.F9.F91 = "123"
174200

175201
// get the map from custom tags
176202
tagName = "custom"
@@ -212,7 +238,13 @@ This will produce a result similar to:
212238
"name": "world",
213239
"value": 2
214240
}
215-
]
241+
],
242+
"omit": {
243+
"f8": false,
244+
"f9": {
245+
"f91": "123"
246+
}
247+
}
216248
}
217249
```
218250
---

map.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
var tagName = "" // initialise the struct tag value
1111

12+
const omitEmptyTagOption = "omitempty" // omit values with this tag option if empty
13+
1214
// getMapOfAllKeyValues builds a map of the fully specified key and the value from the struct tag
1315
// the struct tags with the full dot notation will be used as the key, and the value as the value
1416
// slices will be also be maps
@@ -65,10 +67,17 @@ func getMapOfAllKeyValues(s interface{}) *map[string]interface{} {
6567
continue
6668
}
6769
} else {
70+
// omitempty tag passed?
71+
tag, shouldOmitEmpty := shouldOmitEmpty(tag) // overwrite tag
6872
// recursive check nested fields in case this is a struct
6973
if t.Field(i).Kind() == reflect.Struct {
7074
// only check if the value can be obtained without panicking (eg: for unexported fields)
7175
if t.Field(i).CanInterface() {
76+
if shouldOmitEmpty {
77+
if t.Field(i).IsZero() {
78+
continue
79+
}
80+
}
7281
qVars := getMapOfAllKeyValues(t.Field(i).Interface()) //recursive call
7382
if qVars != nil {
7483
for k, v := range *qVars {
@@ -79,6 +88,11 @@ func getMapOfAllKeyValues(s interface{}) *map[string]interface{} {
7988
} else {
8089
// only check if the value can be obtained without panicking (eg: for unexported fields)
8190
if t.Field(i).CanInterface() {
91+
if shouldOmitEmpty {
92+
if t.Field(i).IsZero() {
93+
continue
94+
}
95+
}
8296
vars[tag] = t.Field(i).Interface()
8397
}
8498
}
@@ -119,6 +133,20 @@ func getMapOfAllKeyValues(s interface{}) *map[string]interface{} {
119133
return &finalMap
120134
}
121135

136+
// shouldOmitEmpty checks if the omitEmptyTagOption option is passed in the tag
137+
// eg: `foo:"bar,omitempty"`
138+
func shouldOmitEmpty(originalTag string) (string, bool) {
139+
if ss := strings.Split(originalTag, ","); len(ss) > 1 {
140+
// TODO: add more validation & error checking
141+
if strings.TrimSpace(ss[1]) == omitEmptyTagOption {
142+
return ss[0], true
143+
}
144+
return ss[0], false
145+
} else {
146+
return originalTag, false
147+
}
148+
}
149+
122150
// buildMap builds the parent map and calls buildNestedMap to create the child maps based on dot notation
123151
func buildMap(s []string, value interface{}, parent *map[string]interface{}) error {
124152
var obj = make(map[string]interface{})
@@ -143,6 +171,10 @@ func ToMap(obj interface{}, tag string) (*map[string]interface{}, error) {
143171
tagName = tag
144172
s := getMapOfAllKeyValues(obj)
145173

174+
if s == nil {
175+
return nil, fmt.Errorf("no valid map could be formed")
176+
}
177+
146178
var parentMap = make(map[string]interface{})
147179
for k, v := range *s {
148180
keys := strings.Split(k, ".")

map_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func TestNilAndUnexportedFields(t *testing.T) {
171171
f1 string `custom:"f1"`
172172
F2 struct {
173173
F21 string `custom:"f21"`
174-
} `custom:"age"`
174+
} `custom:"f2"`
175175
F3 *string `custom:"f3"`
176176
F4 int `custom:"f4"`
177177
F5 interface{} `custom:"f5"`
@@ -204,3 +204,54 @@ func TestNilAndUnexportedFields(t *testing.T) {
204204
// compare
205205
require.JSONEqf(t, expectedJSON, string(actualJSON), "JSON mismatch")
206206
}
207+
208+
// TestOmitEmptyOptionFields calls ToMap function and checks for the expected behaviour
209+
// of passing the omitempty tag option
210+
func TestOmitEmptyOptionFields(t *testing.T) {
211+
type MyStruct struct {
212+
F1 string `custom:"f1,omitempty"`
213+
F2 struct {
214+
F21 string `custom:"f21,omitempty"`
215+
} `custom:"f2"`
216+
F3 *string `custom:"f3, omitempty"` // omitempty with space
217+
F4 int `custom:"f4,omitempty"`
218+
F5 bool `custom:"f5,omitempty"`
219+
F6 interface{} `custom:"f6,omitempty"`
220+
F7 struct {
221+
F71 string `custom:"f71"`
222+
} `custom:"f7,omitempty"`
223+
F8 *bool `custom:"f8,omitempty"`
224+
F9 struct {
225+
F91 string `custom:"f91"`
226+
} `custom:"f9,omitempty"`
227+
}
228+
229+
f := false
230+
obj := MyStruct{
231+
F5: f,
232+
F8: &f,
233+
}
234+
obj.F9.F91 = "123"
235+
236+
// expected response
237+
expectedJSON := `{
238+
"f8": false,
239+
"f9": {
240+
"f91": "123"
241+
}
242+
}
243+
`
244+
245+
// get the map from custom tags
246+
actual, err := ToMap(obj, "custom")
247+
if err != nil {
248+
t.Fail()
249+
}
250+
actualJSON, err := json.Marshal(actual)
251+
if err != nil {
252+
t.Fail()
253+
}
254+
255+
// compare
256+
require.JSONEqf(t, expectedJSON, string(actualJSON), "JSON mismatch")
257+
}

0 commit comments

Comments
 (0)