Skip to content

Struct-to-Map conversion with Deep: true fails to recursively decode slices of pointers to structs #196

Description

@VedantMadane

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()
		}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions