diff --git a/mapstructure.go b/mapstructure.go index 9087fd96..7eff8025 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -1681,6 +1681,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e continue } + // Skip keys already consumed by another field's exact + // match, so a case-insensitive fallback can't claim the + // same key a second time (e.g. fields `Name` and `NAME` + // both matching the key "name"). + if _, unused := dataValKeysUnused[dataValKey.Interface()]; !unused { + continue + } + if d.config.MatchName(mK, fieldName) { rawMapKey = dataValKey rawMapVal = dataVal.MapIndex(dataValKey) diff --git a/mapstructure_test.go b/mapstructure_test.go index baf40dfe..55a0ae0d 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -4673,3 +4673,25 @@ func TestUnmarshaler_StructToMap(t *testing.T) { t.Errorf("expected Age 30, got %v", result["Age"]) } } + +func TestDecode_caseInsensitiveDuplicateKey(t *testing.T) { + // A case-insensitive fallback must not claim a key that another field + // already matched exactly: decoding {"name": ...} into a struct with both + // `name` and `NAME` fields should populate only the exact match. + type Result struct { + Name string `mapstructure:"name"` + NAME string `mapstructure:"NAME"` + } + + var result Result + if err := Decode(map[string]interface{}{"name": "lower"}, &result); err != nil { + t.Fatalf("got an err: %s", err) + } + + if result.Name != "lower" { + t.Errorf("Name should be 'lower', got: %#v", result.Name) + } + if result.NAME != "" { + t.Errorf("NAME should be empty, got: %#v", result.NAME) + } +}