Skip to content

Commit dee4661

Browse files
Merge pull request #59 from DarkiT/main
feat(decoder): support multiple tag names in order
2 parents 6471aa6 + 5605df4 commit dee4661

File tree

2 files changed

+279
-10
lines changed

2 files changed

+279
-10
lines changed

mapstructure.go

Lines changed: 54 additions & 10 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
@@ -699,7 +701,7 @@ func (d *Decoder) decodeString(name string, data any, val reflect.Value) error {
699701
case reflect.Uint8:
700702
var uints []uint8
701703
if dataKind == reflect.Array {
702-
uints = make([]uint8, dataVal.Len(), dataVal.Len())
704+
uints = make([]uint8, dataVal.Len())
703705
for i := range uints {
704706
uints[i] = dataVal.Index(i).Interface().(uint8)
705707
}
@@ -1091,7 +1093,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
10911093
)
10921094
}
10931095

1094-
tagValue := f.Tag.Get(d.config.TagName)
1096+
tagValue, _ := getTagValue(f, d.config.TagName)
10951097
keyName := d.config.MapFieldName(f.Name)
10961098

10971099
if tagValue == "" && d.config.IgnoreUntaggedFields {
@@ -1112,12 +1114,12 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
11121114
continue
11131115
}
11141116
// If "omitempty" is specified in the tag, it ignores empty values.
1115-
if strings.Index(tagValue[index+1:], "omitempty") != -1 && isEmptyValue(v) {
1117+
if strings.Contains(tagValue[index+1:], "omitempty") && isEmptyValue(v) {
11161118
continue
11171119
}
11181120

11191121
// If "omitzero" is specified in the tag, it ignores zero values.
1120-
if strings.Index(tagValue[index+1:], "omitzero") != -1 && v.IsZero() {
1122+
if strings.Contains(tagValue[index+1:], "omitzero") && v.IsZero() {
11211123
continue
11221124
}
11231125

@@ -1137,7 +1139,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
11371139
)
11381140
}
11391141
} else {
1140-
if strings.Index(tagValue[index+1:], "remain") != -1 {
1142+
if strings.Contains(tagValue[index+1:], "remain") {
11411143
if v.Kind() != reflect.Map {
11421144
return newDecodeError(
11431145
name+"."+f.Name,
@@ -1153,7 +1155,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
11531155
}
11541156
}
11551157

1156-
deep = deep || strings.Index(tagValue[index+1:], "deep") != -1
1158+
deep = deep || strings.Contains(tagValue[index+1:], "deep")
11571159

11581160
if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" {
11591161
keyName = keyNameTagValue
@@ -1543,7 +1545,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
15431545
remain := false
15441546

15451547
// We always parse the tags cause we're looking for other tags too
1546-
tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",")
1548+
tagParts := getTagParts(fieldType, d.config.TagName)
1549+
if len(tagParts) == 0 {
1550+
tagParts = []string{""}
1551+
}
15471552
for _, tag := range tagParts[1:] {
15481553
if tag == d.config.SquashTagOption {
15491554
squash = true
@@ -1600,7 +1605,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
16001605
field, fieldValue := f.field, f.val
16011606
fieldName := field.Name
16021607

1603-
tagValue := field.Tag.Get(d.config.TagName)
1608+
tagValue, _ := getTagValue(field, d.config.TagName)
16041609
if tagValue == "" && d.config.IgnoreUntaggedFields {
16051610
continue
16061611
}
@@ -1784,7 +1789,7 @@ func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool,
17841789
if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields
17851790
return true
17861791
}
1787-
if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside
1792+
if checkMapstructureTags && hasAnyTag(f, tagName) { // check for mapstructure tags inside
17881793
return true
17891794
}
17901795
}
@@ -1812,3 +1817,42 @@ func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Val
18121817
return v
18131818
}
18141819
}
1820+
1821+
func hasAnyTag(field reflect.StructField, tagName string) bool {
1822+
_, ok := getTagValue(field, tagName)
1823+
return ok
1824+
}
1825+
1826+
func getTagParts(field reflect.StructField, tagName string) []string {
1827+
tagValue, ok := getTagValue(field, tagName)
1828+
if !ok {
1829+
return nil
1830+
}
1831+
return strings.Split(tagValue, ",")
1832+
}
1833+
1834+
func getTagValue(field reflect.StructField, tagName string) (string, bool) {
1835+
for _, name := range splitTagNames(tagName) {
1836+
if tag := field.Tag.Get(name); tag != "" {
1837+
return tag, true
1838+
}
1839+
}
1840+
return "", false
1841+
}
1842+
1843+
func splitTagNames(tagName string) []string {
1844+
if tagName == "" {
1845+
return []string{"mapstructure"}
1846+
}
1847+
parts := strings.Split(tagName, ",")
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+
}
1855+
}
1856+
1857+
return result
1858+
}

mapstructure_test.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3235,6 +3235,231 @@ func TestDecoder_IgnoreUntaggedFieldsWithStruct(t *testing.T) {
32353235
}
32363236
}
32373237

3238+
func TestDecoder_MultiTagInline(t *testing.T) {
3239+
type Inner struct {
3240+
A int `yaml:"a"`
3241+
}
3242+
3243+
type Wrap struct {
3244+
Inner `yaml:",inline"`
3245+
}
3246+
3247+
input := map[string]any{"a": 1}
3248+
var result Wrap
3249+
3250+
dec, err := NewDecoder(&DecoderConfig{
3251+
TagName: "config,yaml",
3252+
SquashTagOption: "inline",
3253+
WeaklyTypedInput: true,
3254+
Result: &result,
3255+
})
3256+
if err != nil {
3257+
t.Fatalf("NewDecoder error: %v", err)
3258+
}
3259+
3260+
if err := dec.Decode(input); err != nil {
3261+
t.Fatalf("Decode error: %v", err)
3262+
}
3263+
3264+
if result.Inner.A != 1 {
3265+
t.Fatalf("expected inline field A=1, got %d", result.Inner.A)
3266+
}
3267+
}
3268+
3269+
func TestDecoder_MultiTagRemain(t *testing.T) {
3270+
type Wrap struct {
3271+
Known string `yaml:"known"`
3272+
Extra map[string]any `yaml:",remain"`
3273+
}
3274+
3275+
input := map[string]any{
3276+
"known": "ok",
3277+
"extra1": "v1",
3278+
"extra2": 2,
3279+
}
3280+
var result Wrap
3281+
3282+
dec, err := NewDecoder(&DecoderConfig{
3283+
TagName: "config,yaml",
3284+
WeaklyTypedInput: true,
3285+
Result: &result,
3286+
})
3287+
if err != nil {
3288+
t.Fatalf("NewDecoder error: %v", err)
3289+
}
3290+
3291+
if err := dec.Decode(input); err != nil {
3292+
t.Fatalf("Decode error: %v", err)
3293+
}
3294+
3295+
if result.Known != "ok" {
3296+
t.Fatalf("expected Known=ok, got %q", result.Known)
3297+
}
3298+
if result.Extra == nil || len(result.Extra) != 2 {
3299+
t.Fatalf("expected Extra to contain 2 items, got %v", result.Extra)
3300+
}
3301+
if result.Extra["extra1"] != "v1" {
3302+
t.Fatalf("expected extra1=v1, got %v", result.Extra["extra1"])
3303+
}
3304+
}
3305+
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+
32383463
func TestDecoder_DecodeNilOption(t *testing.T) {
32393464
t.Parallel()
32403465

0 commit comments

Comments
 (0)