go: 1.10
viper: 1.0.2
when a field to be parsed is time.Time viper fails with error expected a map, got 'string'
Test to reproduce (can be added in viper_test.go )
func TestParseNestedTime(t *testing.T) {
type duration struct {
Moment time.Time
}
type item struct {
Name string
Moment time.Time
Nested duration
}
config := `[[parent]]
moment="2011-10-09"
[parent.nested]
moment="2009-08-07"
`
initConfig("toml", config)
var items []item
err := v.UnmarshalKey("parent", &items)
if err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}
var m time.Time
assert.Equal(t, 1, len(items))
m, _ = time.Parse("2016-01-02", "2011-10-09")
assert.Equal(t, m, items[0].Moment)
m, _ = time.Parse("2016-01-02", "2009-08-07")
assert.Equal(t, m, items[0].Nested.Moment)
}
result:
--- FAIL: TestParseNestedTime (0.00s)
viper_test.go:1193: unable to decode into struct, 2 error(s) decoding:
* '[0].Moment' expected a map, got 'string'
* '[0].Nested.Moment' expected a map, got 'string'
FAIL
FAIL github.com/spf13/viper 0.033s
Is there a workaround for this?
@ethanmick my workaround now is to define the field as string and then parse it into date "manually"
Bumped into this, things have moved on:
The unmarshal method signature isn't quite as presently decribed in the docs (it is viper.Unmarshal(rawVal interface{}, opts ...DecoderConfigOption) error) so with options config glued together with something like (fiddled to handle empty strings below) mapstructure.StringToTimeHookFunc you can configure the decode and get what you want.
err := viper.GetViper().Unmarshal(&config, func(m *mapstructure.DecoderConfig) {
m.DecodeHook = mapstructure.ComposeDecodeHookFunc(
func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Time{}) {
return data, nil
}
asString := data.(string)
if asString == "" {
return time.Time{}, nil
}
// Convert it by parsing
return time.Parse("2006-01-02", asString)
},
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
)
})
if err != nil {
log.Fatal().Err(err).Msg("Failed to unmarshal config")
}
Thanks @lachlanmunro. FYI a more comprehensive time decoder function is given here: https://github.com/mitchellh/mapstructure/issues/159#issuecomment-482201507
Most helpful comment
@ethanmick my workaround now is to define the field as string and then parse it into date "manually"