Description
When converting a Go struct into a map (i.e. decoding a struct value into a target map[string]any) with DecoderConfig.Deep set to true:
- Slices of structs (
[]MyStruct) are recursively decoded into slices of maps ([]map[string]any).
- Slices of pointers to structs (
[]*MyStruct) are not recursively decoded. The resulting map retains the slice of pointers to the struct objects raw ([]*MyStruct).
Root Cause
In mapstructure.go, inside func (d *Decoder) decodeMapFromStruct, the logic handling reflect.Slice fields is:
case reflect.Slice:
if deep {
var childType reflect.Type
switch v.Type().Elem().Kind() {
case reflect.Struct:
childType = reflect.TypeOf(map[string]any{})
default:
childType = v.Type().Elem()
}
- When the field type is
[]WeaponState, v.Type().Elem().Kind() returns reflect.Struct. The decoder changes the child element type to map[string]any and recursively decodes it.
- When the field type is
[]*WeaponState, v.Type().Elem().Kind() returns reflect.Ptr. The decoder falls into the default case, keeping the element type as *WeaponState, bypassing the recursive map transformation.
Reproduction Code
package main
import (
"fmt"
"github.com/go-viper/mapstructure/v2"
)
type WeaponState struct {
Id int
}
type Player struct {
WeaponsSlice []WeaponState
WeaponsPtrSlice []*WeaponState
}
func main() {
p := Player{
WeaponsSlice: []WeaponState{
{Id: 1},
},
WeaponsPtrSlice: []*WeaponState{
{Id: 2},
},
}
var result map[string]any
config := &mapstructure.DecoderConfig{
Result: &result,
Deep: true,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
panic(err)
}
err = decoder.Decode(p)
if err != nil {
panic(err)
}
fmt.Printf("WeaponsSlice: %T -> %+v\n", result["WeaponsSlice"], result["WeaponsSlice"])
fmt.Printf("WeaponsPtrSlice: %T -> %+v\n", result["WeaponsPtrSlice"], result["WeaponsPtrSlice"])
}
Output:
WeaponsSlice: []map[string]interface {} -> [map[Id:1]]
WeaponsPtrSlice: []*main.WeaponState -> [0xc0000a60a8]
Proposed Fix
We should resolve the underlying element type by checking if it is a pointer, and check the dereferenced element's kind instead:
case reflect.Slice:
if deep {
var childType reflect.Type
elemType := v.Type().Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
switch elemType.Kind() {
case reflect.Struct:
childType = reflect.TypeOf(map[string]any{})
default:
childType = v.Type().Elem()
}
Description
When converting a Go struct into a map (i.e. decoding a struct value into a target
map[string]any) withDecoderConfig.Deepset totrue:[]MyStruct) are recursively decoded into slices of maps ([]map[string]any).[]*MyStruct) are not recursively decoded. The resulting map retains the slice of pointers to the struct objects raw ([]*MyStruct).Root Cause
In
mapstructure.go, insidefunc (d *Decoder) decodeMapFromStruct, the logic handlingreflect.Slicefields is:[]WeaponState,v.Type().Elem().Kind()returnsreflect.Struct. The decoder changes the child element type tomap[string]anyand recursively decodes it.[]*WeaponState,v.Type().Elem().Kind()returnsreflect.Ptr. The decoder falls into thedefaultcase, keeping the element type as*WeaponState, bypassing the recursive map transformation.Reproduction Code
Output:
Proposed Fix
We should resolve the underlying element type by checking if it is a pointer, and check the dereferenced element's kind instead: