Skip to content

Commit dbffaaa

Browse files
committed
chore: add more tests and clarification to the documentation
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
1 parent 3b2e16c commit dbffaaa

File tree

2 files changed

+112
-1
lines changed

2 files changed

+112
-1
lines changed

mapstructure.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,14 +311,28 @@ type DecoderConfig struct {
311311
// MatchName is the function used to match the map key to the struct
312312
// field name or tag. Defaults to `strings.EqualFold`. This can be used
313313
// to implement case-sensitive tag values, support snake casing, etc.
314+
//
315+
// MatchName is used as a fallback comparison when the direct key lookup fails.
316+
// See also MapFieldName for transforming field names before lookup.
314317
MatchName func(mapKey, fieldName string) bool
315318

316319
// DecodeNil, if set to true, will cause the DecodeHook (if present) to run
317320
// even if the input is nil. This can be used to provide default values.
318321
DecodeNil bool
319322

320323
// MapFieldName is the function used to convert the struct field name to the map's key name.
321-
// This can be used to support snake casing, etc., without explicitly mapping each name.
324+
//
325+
// This is useful for automatically converting between naming conventions without
326+
// explicitly tagging each field. For example, to convert Go's PascalCase field names
327+
// to snake_case map keys:
328+
//
329+
// MapFieldName: func(s string) string {
330+
// return strcase.ToSnake(s)
331+
// }
332+
//
333+
// When decoding from a map to a struct, the transformed field name is used for
334+
// the initial lookup. If not found, MatchName is used as a fallback comparison.
335+
// Explicit struct tags always take precedence over MapFieldName.
322336
MapFieldName func(string) string
323337
}
324338

mapstructure_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3688,6 +3688,8 @@ type TestMapFieldName struct {
36883688
}
36893689

36903690
func TestDecoder_MapFieldNameMapFromStruct(t *testing.T) {
3691+
t.Parallel()
3692+
36913693
var structKeys map[string]any
36923694

36933695
decoder, err := NewDecoder(&DecoderConfig{
@@ -3723,6 +3725,8 @@ func TestDecoder_MapFieldNameMapFromStruct(t *testing.T) {
37233725
}
37243726

37253727
func TestDecoder_MapFieldNameStructFromMap(t *testing.T) {
3728+
t.Parallel()
3729+
37263730
foo := TestMapFieldName{}
37273731

37283732
decoder, err := NewDecoder(&DecoderConfig{
@@ -3759,3 +3763,96 @@ func TestDecoder_MapFieldNameStructFromMap(t *testing.T) {
37593763
t.Fatal("expected Username to be bar")
37603764
}
37613765
}
3766+
3767+
func TestDecoder_MapFieldNameWithMatchName(t *testing.T) {
3768+
t.Parallel()
3769+
3770+
type Target struct {
3771+
HostName string
3772+
Username string
3773+
}
3774+
3775+
input := map[string]any{
3776+
"HOST_NAME": "server1",
3777+
"user_name": "admin",
3778+
}
3779+
3780+
var result Target
3781+
decoder, err := NewDecoder(&DecoderConfig{
3782+
Result: &result,
3783+
MapFieldName: func(s string) string {
3784+
// Convert HostName -> host_name, Username -> user_name
3785+
if s == "HostName" {
3786+
return "host_name"
3787+
}
3788+
if s == "Username" {
3789+
return "user_name"
3790+
}
3791+
return s
3792+
},
3793+
MatchName: strings.EqualFold, // Case-insensitive fallback
3794+
})
3795+
if err != nil {
3796+
t.Fatalf("err: %s", err)
3797+
}
3798+
3799+
err = decoder.Decode(input)
3800+
if err != nil {
3801+
t.Fatalf("err: %s", err)
3802+
}
3803+
3804+
// HOST_NAME should match host_name via case-insensitive MatchName
3805+
if result.HostName != "server1" {
3806+
t.Fatalf("expected HostName to be 'server1', got '%s'", result.HostName)
3807+
}
3808+
3809+
// user_name matches exactly
3810+
if result.Username != "admin" {
3811+
t.Fatalf("expected Username to be 'admin', got '%s'", result.Username)
3812+
}
3813+
}
3814+
3815+
func TestDecoder_MapFieldNameWithIgnoreUntaggedFields(t *testing.T) {
3816+
t.Parallel()
3817+
3818+
type Target struct {
3819+
TaggedField string `mapstructure:"tagged_field"`
3820+
UntaggedField string
3821+
}
3822+
3823+
input := map[string]any{
3824+
"tagged_field": "tagged_value",
3825+
"untagged_field": "untagged_value",
3826+
}
3827+
3828+
var result Target
3829+
decoder, err := NewDecoder(&DecoderConfig{
3830+
Result: &result,
3831+
IgnoreUntaggedFields: true,
3832+
MapFieldName: func(s string) string {
3833+
// This would convert UntaggedField -> untagged_field,
3834+
// but IgnoreUntaggedFields takes precedence
3835+
if s == "UntaggedField" {
3836+
return "untagged_field"
3837+
}
3838+
return s
3839+
},
3840+
})
3841+
if err != nil {
3842+
t.Fatalf("err: %s", err)
3843+
}
3844+
3845+
err = decoder.Decode(input)
3846+
if err != nil {
3847+
t.Fatalf("err: %s", err)
3848+
}
3849+
3850+
if result.TaggedField != "tagged_value" {
3851+
t.Fatalf("expected TaggedField to be 'tagged_value', got '%s'", result.TaggedField)
3852+
}
3853+
3854+
// UntaggedField should remain empty because IgnoreUntaggedFields is true
3855+
if result.UntaggedField != "" {
3856+
t.Fatalf("expected UntaggedField to be empty due to IgnoreUntaggedFields, got '%s'", result.UntaggedField)
3857+
}
3858+
}

0 commit comments

Comments
 (0)