Repository: mitchellh/mapstructure
Pull request #225 (2020-12-18 17:16:19)

Add ErrorUnset option to DecoderConfig and Unset array to Metddata
This commit extends the DecoderConfig with an option to fail decoding
if there exist fields in the target struct, which haven't been set
during decoding due to a missing, corresponding value in the input map.

Accordingly, the Metadata has been extended with an Unset array to
receive all field names which have not been set during decoding.


Patch:
diff --git a/mapstructure.go b/mapstructure.go
index cee7dc0..73d2e18 100644
--- a/mapstructure.go
+++ b/mapstructure.go
@@ -200,6 +200,11 @@ type DecoderConfig struct {
 	// (extra keys).
 	ErrorUnused bool
 
+	// If ErrorUnset is true, then it is an error for there to exist
+	// fields in the result that were not set in the decoding process
+	// (extra fields).
+	ErrorUnset bool
+
 	// ZeroFields, if set to true, will zero fields before writing them.
 	// For example, a map will be emptied before decoded values are put in
 	// it. If this is false, a map will be merged.
@@ -264,6 +269,11 @@ type Metadata struct {
 	// Unused is a slice of keys that were found in the raw value but
 	// weren't decoded since there was no matching field in the result interface
 	Unused []string
+
+	// Unset is a slice of field names that were found in the result interface
+	// but weren't set in the decoding process since there was no matching value
+	// in the input
+	Unset []string
 }
 
 // Decode takes an input structure and uses reflection to translate it to
@@ -355,6 +365,10 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
 		if config.Metadata.Unused == nil {
 			config.Metadata.Unused = make([]string, 0)
 		}
+
+		if config.Metadata.Unset == nil {
+			config.Metadata.Unset = make([]string, 0)
+		}
 	}
 
 	if config.TagName == "" {
@@ -1225,6 +1239,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
 		dataValKeysUnused[dataValKey.Interface()] = struct{}{}
 	}
 
+	targetValKeysUnused := make(map[interface{}]struct{})
 	errors := make([]string, 0)
 
 	// This slice will keep track of all the structs we'll be decoding.
@@ -1329,7 +1344,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
 
 			if !rawMapVal.IsValid() {
 				// There was no matching key in the map for the value in
-				// the struct. Just ignore.
+				// the struct. Remember it for potential errors and metadata.
+				targetValKeysUnused[fieldName] = struct{}{}
 				continue
 			}
 		}
@@ -1389,6 +1405,17 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
 		errors = appendErrors(errors, err)
 	}
 
+	if d.config.ErrorUnset && len(targetValKeysUnused) > 0 {
+		keys := make([]string, 0, len(targetValKeysUnused))
+		for rawKey := range targetValKeysUnused {
+			keys = append(keys, rawKey.(string))
+		}
+		sort.Strings(keys)
+
+		err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", "))
+		errors = appendErrors(errors, err)
+	}
+
 	if len(errors) > 0 {
 		return &Error{errors}
 	}
@@ -1403,6 +1430,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
 
 			d.config.Metadata.Unused = append(d.config.Metadata.Unused, key)
 		}
+		for rawKey := range targetValKeysUnused {
+			key := rawKey.(string)
+			if name != "" {
+				key = name + "." + key
+			}
+
+			d.config.Metadata.Unset = append(d.config.Metadata.Unset, key)
+		}
 	}
 
 	return nil


Test patch:
diff --git a/mapstructure_test.go b/mapstructure_test.go
index ccd474c..291e131 100644
--- a/mapstructure_test.go
+++ b/mapstructure_test.go
@@ -1225,6 +1225,30 @@ func TestDecoder_ErrorUnused_NotSetable(t *testing.T) {
 		t.Fatal("expected error")
 	}
 }
+func TestDecoder_ErrorUnset(t *testing.T) {
+	t.Parallel()
+
+	input := map[string]interface{}{
+		"vstring": "hello",
+		"foo":     "bar",
+	}
+
+	var result Basic
+	config := &DecoderConfig{
+		ErrorUnset: true,
+		Result:     &result,
+	}
+
+	decoder, err := NewDecoder(config)
+	if err != nil {
+		t.Fatalf("err: %s", err)
+	}
+
+	err = decoder.Decode(input)
+	if err == nil {
+		t.Fatal("expected error")
+	}
+}
 
 func TestMap(t *testing.T) {
 	t.Parallel()
@@ -2192,6 +2216,14 @@ func TestMetadata(t *testing.T) {
 	if !reflect.DeepEqual(md.Unused, expectedUnused) {
 		t.Fatalf("bad unused: %#v", md.Unused)
 	}
+
+	expectedUnset := []string{
+		"Vbar.Vbool", "Vbar.Vdata", "Vbar.Vextra", "Vbar.Vfloat", "Vbar.Vint",
+		"Vbar.VjsonFloat", "Vbar.VjsonInt", "Vbar.VjsonNumber"}
+	sort.Strings(md.Unset)
+	if !reflect.DeepEqual(md.Unset, expectedUnset) {
+		t.Fatalf("bad unset: %#v", md.Unset)
+	}
 }
 
 func TestMetadata_Embedded(t *testing.T) {