-
Notifications
You must be signed in to change notification settings - Fork 36
Description
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:
Lines 502 to 516 in 8cfa816
| 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.