From 5c14736b1926461a5e9190b16c3a44ba37d13af8 Mon Sep 17 00:00:00 2001 From: AgentGoose32 Date: Tue, 28 Apr 2026 18:15:14 -0400 Subject: [PATCH] Support struct tags for map keys --- issue255_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ map.go | 9 +++++++++ merge.go | 8 ++++++++ 3 files changed, 58 insertions(+) create mode 100644 issue255_test.go diff --git a/issue255_test.go b/issue255_test.go new file mode 100644 index 0000000..7e359cb --- /dev/null +++ b/issue255_test.go @@ -0,0 +1,41 @@ +package mergo_test + +import ( + "testing" + + "dario.cat/mergo" +) + +type issue255Person struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name,omitempty"` + Password string `json:"-"` + Age int +} + +func TestIssue255MapStructToMapWithTaggedKeys(t *testing.T) { + person := issue255Person{ + FirstName: "Ada", + LastName: "Lovelace", + Password: "secret", + Age: 36, + } + + actual := map[string]interface{}{} + if err := mergo.Map(&actual, person, mergo.WithMapKeyTag("json")); err != nil { + t.Fatal(err) + } + + if actual["first_name"] != "Ada" { + t.Fatalf("expected first_name key to be mapped from tag, got %#v", actual) + } + if actual["last_name"] != "Lovelace" { + t.Fatalf("expected tag name to be used, got %#v", actual) + } + if actual["password"] != "secret" { + t.Fatalf("expected '-' tag to fall back to default field name, got %#v", actual) + } + if actual["age"] != 36 { + t.Fatalf("expected untagged field to fall back to default field name, got %#v", actual) + } +} diff --git a/map.go b/map.go index 759b4f7..2d64228 100644 --- a/map.go +++ b/map.go @@ -11,6 +11,7 @@ package mergo import ( "fmt" "reflect" + "strings" "unicode" "unicode/utf8" ) @@ -58,6 +59,14 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, conf } fieldName := field.Name fieldName = changeInitialCase(fieldName, unicode.ToLower) + if config.mapKeyTag != "" { + if taggedName := field.Tag.Get(config.mapKeyTag); taggedName != "" { + taggedName, _, _ = strings.Cut(taggedName, ",") + if taggedName != "" && taggedName != "-" { + fieldName = taggedName + } + } + } if _, ok := dstMap[fieldName]; !ok || (!isEmptyValue(reflect.ValueOf(src.Field(i).Interface()), !config.ShouldNotDereference) && overwrite) || config.overwriteWithEmptyValue { dstMap[fieldName] = src.Field(i).Interface() } diff --git a/merge.go b/merge.go index 5488338..0cba952 100644 --- a/merge.go +++ b/merge.go @@ -46,6 +46,7 @@ type Config struct { overwriteWithEmptyValue bool overwriteSliceWithEmptyValue bool sliceDeepCopy bool + mapKeyTag string } type Transformers interface { @@ -367,6 +368,13 @@ func WithSliceDeepCopy(config *Config) { config.Overwrite = true } +// WithMapKeyTag uses the named struct tag value as the destination map key when mapping a struct to map. +func WithMapKeyTag(tag string) func(*Config) { + return func(config *Config) { + config.mapKeyTag = tag + } +} + func merge(dst, src interface{}, opts ...func(*Config)) error { if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr { return ErrNonPointerArgument