Skip to content

Decoding from Type to map[string]interface{} can't handle transformations which change the interface type #84

@wrouesnel

Description

@wrouesnel

When decoding from a type back to a map[string]interface{}, types which have decode hooks which change the returned interface{} type don't decode properly.

The following reproduces the problem (playground)

package main

import (
	"errors"
	"fmt"
	"reflect"

	"github.com/go-viper/mapstructure/v2"
)

type ReallySpecial struct {
	X int
	Y int
}

func (t *ReallySpecial) MapStructureEncode() (interface{}, error) {
	return []int{t.X, t.Y}, nil
}

type MapStructureEncoder interface {
	MapStructureEncode() (interface{}, error)
}

func MapStructureEncodeHookFunc() mapstructure.DecodeHookFuncType {
	return func(
		f reflect.Type,
		t reflect.Type,
		data interface{}) (interface{}, error) {
		marshaller, ok := data.(MapStructureEncoder)
		if !ok {
			return data, nil
		}
		result, err := marshaller.MapStructureEncode()
		if err != nil {
			return nil, errors.Join(errors.New("MapStructureDecode function returned error"), err)
		}
		return result, nil
	}
}

type T struct {
	Special ReallySpecial `mapstructure:"special"`
}

func main() {
	output := new(map[string]interface{})

	x := &T{
		Special: ReallySpecial{
			X: 14,
			Y: 21,
		},
	}

	encoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		ErrorUnused: true,
		DecodeHook:  MapStructureEncodeHookFunc(),
		Result:      output,
	})

	err = encoder.Decode(x)
	fmt.Println(err)
	fmt.Println(output)
}

The issue seems to be this code here:

if d.cachedDecodeHook != nil {
// We have a DecodeHook, so let's pre-process the input.
var err error
input, err = d.cachedDecodeHook(inputVal, outVal)
if err != nil {
return fmt.Errorf("error decoding '%s': %w", name, err)
}
}
if isNil(input) {
return nil
}
var err error
addMetaKey := true
switch outputKind {

The problem is that outVal is assumed to be unchanged by changes to the input data produced by the decoding hook. Since the input to the function is a plain structure, mapstructure decides to try and decode outval as a map[string]interface{}. However, my decoding hook transforms that into a list of ints (because it's serialization is different to it's concrete type).

This limitation prevents proper two-way serialization being implemented because it isn't possible to express the reverse transform - e.g.

I can start out with:

special: [14,21]

do a decode with YAML into

map[string]interface{}{
  "special": []int{14,21}
}

and finally decode with mapstructure into:

&T{
  Special: ReallySpecial{
    X: 14,
    Y: 21,
  },
}

but I cannot use decode hooks in the expected way to go backwards.

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