Skip to content

Commit 5605df4

Browse files
committed
chore: cover more test cases, fix edge cases, add docs
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
1 parent 6166631 commit 5605df4

File tree

2 files changed

+169
-4
lines changed

2 files changed

+169
-4
lines changed

mapstructure.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,9 @@ type DecoderConfig struct {
297297
Result any
298298

299299
// The tag name that mapstructure reads for field names. This
300-
// defaults to "mapstructure"
300+
// defaults to "mapstructure". Multiple tag names can be specified
301+
// as a comma-separated list (e.g., "yaml,json"), and the first
302+
// matching non-empty tag will be used.
301303
TagName string
302304

303305
// The option of the value in the tag that indicates a field should
@@ -1843,8 +1845,14 @@ func splitTagNames(tagName string) []string {
18431845
return []string{"mapstructure"}
18441846
}
18451847
parts := strings.Split(tagName, ",")
1846-
for i, name := range parts {
1847-
parts[i] = strings.TrimSpace(name)
1848+
result := make([]string, 0, len(parts))
1849+
1850+
for _, name := range parts {
1851+
name = strings.TrimSpace(name)
1852+
if name != "" {
1853+
result = append(result, name)
1854+
}
18481855
}
1849-
return parts
1856+
1857+
return result
18501858
}

mapstructure_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3303,6 +3303,163 @@ func TestDecoder_MultiTagRemain(t *testing.T) {
33033303
}
33043304
}
33053305

3306+
func TestDecoder_MultiTagBasic(t *testing.T) {
3307+
type Person struct {
3308+
Name string `yaml:"name"`
3309+
Age int `json:"age"`
3310+
Email string `config:"email_address"`
3311+
}
3312+
3313+
input := map[string]any{
3314+
"name": "Alice",
3315+
"age": 30,
3316+
"email_address": "alice@example.com",
3317+
}
3318+
var result Person
3319+
3320+
dec, err := NewDecoder(&DecoderConfig{
3321+
TagName: "yaml,json,config",
3322+
Result: &result,
3323+
})
3324+
if err != nil {
3325+
t.Fatalf("NewDecoder error: %v", err)
3326+
}
3327+
3328+
if err := dec.Decode(input); err != nil {
3329+
t.Fatalf("Decode error: %v", err)
3330+
}
3331+
3332+
if result.Name != "Alice" {
3333+
t.Fatalf("expected Name=Alice, got %q", result.Name)
3334+
}
3335+
if result.Age != 30 {
3336+
t.Fatalf("expected Age=30, got %d", result.Age)
3337+
}
3338+
if result.Email != "alice@example.com" {
3339+
t.Fatalf("expected Email=alice@example.com, got %q", result.Email)
3340+
}
3341+
}
3342+
3343+
func TestDecoder_MultiTagPriority(t *testing.T) {
3344+
// When both tags exist, the first tag name in the list takes precedence
3345+
type Item struct {
3346+
Value string `yaml:"yaml_value" json:"json_value"`
3347+
}
3348+
3349+
input := map[string]any{
3350+
"yaml_value": "from_yaml",
3351+
"json_value": "from_json",
3352+
}
3353+
3354+
// Test yaml,json order - should use yaml tag
3355+
var result1 Item
3356+
dec1, err := NewDecoder(&DecoderConfig{
3357+
TagName: "yaml,json",
3358+
Result: &result1,
3359+
})
3360+
if err != nil {
3361+
t.Fatalf("NewDecoder error: %v", err)
3362+
}
3363+
if err := dec1.Decode(input); err != nil {
3364+
t.Fatalf("Decode error: %v", err)
3365+
}
3366+
if result1.Value != "from_yaml" {
3367+
t.Fatalf("with yaml,json expected Value=from_yaml, got %q", result1.Value)
3368+
}
3369+
3370+
// Test json,yaml order - should use json tag
3371+
var result2 Item
3372+
dec2, err := NewDecoder(&DecoderConfig{
3373+
TagName: "json,yaml",
3374+
Result: &result2,
3375+
})
3376+
if err != nil {
3377+
t.Fatalf("NewDecoder error: %v", err)
3378+
}
3379+
if err := dec2.Decode(input); err != nil {
3380+
t.Fatalf("Decode error: %v", err)
3381+
}
3382+
if result2.Value != "from_json" {
3383+
t.Fatalf("with json,yaml expected Value=from_json, got %q", result2.Value)
3384+
}
3385+
}
3386+
3387+
func TestDecoder_MultiTagWhitespace(t *testing.T) {
3388+
type Person struct {
3389+
Name string `yaml:"name"`
3390+
Age int `json:"age"`
3391+
}
3392+
3393+
input := map[string]any{
3394+
"name": "Bob",
3395+
"age": 25,
3396+
}
3397+
var result Person
3398+
3399+
// Test with whitespace around tag names
3400+
dec, err := NewDecoder(&DecoderConfig{
3401+
TagName: " yaml , json ",
3402+
Result: &result,
3403+
})
3404+
if err != nil {
3405+
t.Fatalf("NewDecoder error: %v", err)
3406+
}
3407+
3408+
if err := dec.Decode(input); err != nil {
3409+
t.Fatalf("Decode error: %v", err)
3410+
}
3411+
3412+
if result.Name != "Bob" {
3413+
t.Fatalf("expected Name=Bob, got %q", result.Name)
3414+
}
3415+
if result.Age != 25 {
3416+
t.Fatalf("expected Age=25, got %d", result.Age)
3417+
}
3418+
}
3419+
3420+
func TestDecoder_MultiTagEmptyNames(t *testing.T) {
3421+
type Person struct {
3422+
Name string `mapstructure:"name"`
3423+
}
3424+
3425+
input := map[string]any{
3426+
"name": "Charlie",
3427+
}
3428+
3429+
tests := []struct {
3430+
name string
3431+
tagName string
3432+
}{
3433+
{"leading comma", ",yaml"},
3434+
{"trailing comma", "yaml,"},
3435+
{"multiple commas", ",,yaml,,"},
3436+
{"only commas", ",,,"},
3437+
{"empty with spaces", " , , "},
3438+
}
3439+
3440+
for _, tc := range tests {
3441+
t.Run(tc.name, func(t *testing.T) {
3442+
var result Person
3443+
dec, err := NewDecoder(&DecoderConfig{
3444+
TagName: tc.tagName,
3445+
Result: &result,
3446+
})
3447+
if err != nil {
3448+
t.Fatalf("NewDecoder error: %v", err)
3449+
}
3450+
3451+
if err := dec.Decode(input); err != nil {
3452+
t.Fatalf("Decode error: %v", err)
3453+
}
3454+
3455+
// With invalid/empty tag names, should fall back to mapstructure
3456+
if result.Name != "Charlie" {
3457+
t.Fatalf("expected Name=Charlie (fallback to mapstructure), got %q", result.Name)
3458+
}
3459+
})
3460+
}
3461+
}
3462+
33063463
func TestDecoder_DecodeNilOption(t *testing.T) {
33073464
t.Parallel()
33083465

0 commit comments

Comments
 (0)