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:
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.
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)
The issue seems to be this code here:
mapstructure/mapstructure.go
Lines 502 to 516 in 8cfa816
The problem is that
outValis 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 amap[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:
do a decode with YAML into
and finally decode with mapstructure into:
but I cannot use decode hooks in the expected way to go backwards.