From d555987c20e9ac477ef7eb445cde7de4d6dfbd7f Mon Sep 17 00:00:00 2001 From: kigland Date: Sat, 30 May 2026 14:25:03 +0800 Subject: [PATCH] fix: don't let case-insensitive fallback reuse an exactly-matched key When a struct has fields whose names differ only by case (e.g. `name` and `NAME`) and the input map contains one of them (e.g. "name"), the field with the exact match consumed the key and the case-insensitive fallback then matched the same key again, populating both fields. Skip keys that another field already removed from the unused set so each map key is consumed by at most one field. --- mapstructure.go | 8 ++++++++ mapstructure_test.go | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) 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) + } +}