Repository: mitchellh/mapstructure
Pull request #145 (2018-10-29 15:11:52)

Support to omitempty
Add support to omitempty anotation in order to not add map keys with empty values


Patch:
diff --git a/mapstructure.go b/mapstructure.go
index 256ee63..e49e3a6 100644
--- a/mapstructure.go
+++ b/mapstructure.go
@@ -680,6 +680,18 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
 		tagValue := f.Tag.Get(d.config.TagName)
 		tagParts := strings.Split(tagValue, ",")
 
+		// If "omitempty" is specified in the tag, it ignores empty values.
+		omitempty := false
+		for _, tag := range tagParts[1:] {
+			if tag == "omitempty" {
+				omitempty = true
+				break
+			}
+		}
+		if omitempty && isEmptyValue(v) {
+			continue
+		}
+
 		// Determine the name of the key in the map
 		keyName := f.Name
 		if tagParts[0] != "" {
@@ -1133,6 +1145,24 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
 	return nil
 }
 
+func isEmptyValue(v reflect.Value) bool {
+	switch getKind(v) {
+	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+		return v.Len() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Interface, reflect.Ptr:
+		return v.IsNil()
+	}
+	return false
+}
+
 func getKind(val reflect.Value) reflect.Kind {
 	kind := val.Kind()
 


Test patch:
diff --git a/mapstructure_examples_test.go b/mapstructure_examples_test.go
index f17c214..99db9f6 100644
--- a/mapstructure_examples_test.go
+++ b/mapstructure_examples_test.go
@@ -201,3 +201,30 @@ func ExampleDecode_embeddedStruct() {
 	// Output:
 	// Mitchell Hashimoto, San Francisco
 }
+
+func ExampleDecode_omitempty() {
+	// Add omitempty annotation to avoid map keys for empty values
+	type Family struct {
+		LastName string
+	}
+	type Location struct {
+		City string
+	}
+	type Person struct {
+		*Family   `mapstructure:",omitempty"`
+		*Location `mapstructure:",omitempty"`
+		Age       int
+		FirstName string
+	}
+
+	result := &map[string]interface{}{}
+	input := Person{FirstName: "Somebody"}
+	err := Decode(input, &result)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("%+v", result)
+	// Output:
+	// &map[Age:0 FirstName:Somebody]
+}
diff --git a/mapstructure_test.go b/mapstructure_test.go
index 27ac10e..5709c51 100644
--- a/mapstructure_test.go
+++ b/mapstructure_test.go
@@ -137,6 +137,21 @@ type Tagged struct {
 	Value string `mapstructure:"foo"`
 }
 
+type StructWithOmitEmpty struct {
+	VisibleStringField string                 `mapstructure:"visible-string"`
+	OmitStringField    string                 `mapstructure:"omittable-string,omitempty"`
+	VisibleIntField    int                    `mapstructure:"visible-int"`
+	OmitIntField       int                    `mapstructure:"omittable-int,omitempty"`
+	VisibleFloatField  float64                `mapstructure:"visible-float"`
+	OmitFloatField     float64                `mapstructure:"omittable-float,omitempty"`
+	VisibleSliceField  []interface{}          `mapstructure:"visible-slice"`
+	OmitSliceField     []interface{}          `mapstructure:"omittable-slice,omitempty"`
+	VisibleMapField    map[string]interface{} `mapstructure:"visible-map"`
+	OmitMapField       map[string]interface{} `mapstructure:"omittable-map,omitempty"`
+	NestedField        *Nested                `mapstructure:"visible-nested"`
+	OmitNestedField    *Nested                `mapstructure:"omittable-nested,omitempty"`
+}
+
 type TypeConversionResult struct {
 	IntToFloat         float32
 	IntToUint          uint
@@ -1631,6 +1646,32 @@ func TestDecodeTable(t *testing.T) {
 			&map[string]int{},
 			true,
 		},
+		{
+			"struct with omitempty tag return non-empty values",
+			&struct {
+				VisibleField interface{} `mapstructure:"visible"`
+				OmitField    interface{} `mapstructure:"omittable,omitempty"`
+			}{
+				VisibleField: nil,
+				OmitField:    "string",
+			},
+			&map[string]interface{}{},
+			&map[string]interface{}{"visible": nil, "omittable": "string"},
+			false,
+		},
+		{
+			"struct with omitempty tag ignore empty values",
+			&struct {
+				VisibleField interface{} `mapstructure:"visible"`
+				OmitField    interface{} `mapstructure:"omittable,omitempty"`
+			}{
+				VisibleField: nil,
+				OmitField:    nil,
+			},
+			&map[string]interface{}{},
+			&map[string]interface{}{"visible": nil},
+			false,
+		},
 	}
 
 	for _, tt := range tests {
@@ -1915,6 +1956,75 @@ func TestWeakDecodeMetadata(t *testing.T) {
 	}
 }
 
+func TestDecode_StructTaggedWithOmitempty_OmitEmptyValues(t *testing.T) {
+	t.Parallel()
+
+	input := &StructWithOmitEmpty{}
+
+	var emptySlice []interface{}
+	var emptyMap map[string]interface{}
+	var emptyNested *Nested
+	expected := &map[string]interface{}{
+		"visible-string": "",
+		"visible-int":    0,
+		"visible-float":  0.0,
+		"visible-slice":  emptySlice,
+		"visible-map":    emptyMap,
+		"visible-nested": emptyNested,
+	}
+
+	actual := &map[string]interface{}{}
+	Decode(input, actual)
+
+	if !reflect.DeepEqual(actual, expected) {
+		t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual)
+	}
+}
+
+func TestDecode_StructTaggedWithOmitempty_KeepNonEmptyValues(t *testing.T) {
+	t.Parallel()
+
+	input := &StructWithOmitEmpty{
+		VisibleStringField: "",
+		OmitStringField:    "string",
+		VisibleIntField:    0,
+		OmitIntField:       1,
+		VisibleFloatField:  0.0,
+		OmitFloatField:     1.0,
+		VisibleSliceField:  nil,
+		OmitSliceField:     []interface{}{1},
+		VisibleMapField:    nil,
+		OmitMapField:       map[string]interface{}{"k": "v"},
+		NestedField:        nil,
+		OmitNestedField:    &Nested{},
+	}
+
+	var emptySlice []interface{}
+	var emptyMap map[string]interface{}
+	var emptyNested *Nested
+	expected := &map[string]interface{}{
+		"visible-string":   "",
+		"omittable-string": "string",
+		"visible-int":      0,
+		"omittable-int":    1,
+		"visible-float":    0.0,
+		"omittable-float":  1.0,
+		"visible-slice":    emptySlice,
+		"omittable-slice":  []interface{}{1},
+		"visible-map":      emptyMap,
+		"omittable-map":    map[string]interface{}{"k": "v"},
+		"visible-nested":   emptyNested,
+		"omittable-nested": &Nested{},
+	}
+
+	actual := &map[string]interface{}{}
+	Decode(input, actual)
+
+	if !reflect.DeepEqual(actual, expected) {
+		t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual)
+	}
+}
+
 func testSliceInput(t *testing.T, input map[string]interface{}, expected *Slice) {
 	var result Slice
 	err := Decode(input, &result)