I am trying to implement a custom unmarshaler for a YAML config to allow a key to support multiple types (in my personal case, a string and sequence). I am trying to configure that if the YAML value is a string type, I can manipulate it into a sequence (e.g. splitting on spaces, quotes, etc).
package main
import (
"bytes"
"fmt"
"github.com/kr/pretty"
"github.com/spf13/viper"
)
type CommandType []string
type Config struct {
Project string
Alias map[string]struct {
Command CommandType
Environment string
}
}
func (cmd *CommandType) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal(&cmd); err != nil {
return err
}
// Simply overwrite unmarshalled data PoC
*cmd = []string{"overwritten", "values"}
return nil
}
var data = []byte(`---
project: proof-of-concept
alias:
poc:
command: this can be split
environment: poc
other:
command:
- this
- can
- be
- left
environment: poc`)
func main() {
viper.SetConfigType("yaml")
viper.ReadConfig(bytes.NewBuffer(data))
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
panic(fmt.Errorf("Fatal error loading config: %s \n", err))
}
fmt.Printf("%# v\n", pretty.Formatter(cfg))
}
With the above code, I expect an output of:
main.Config{
Project: "proof-of-concept",
Alias: {
"poc": {
Command: {"overwritten", "values"},
Environment: "poc",
},
"other": {
Command: {"overwritten", "values"},
Environment: "poc",
},
},
}
but I get the unchanged original data instead:
main.Config{
Project: "proof-of-concept",
Alias: {
"poc": {
Command: {"this can be split"},
Environment: "poc",
},
"other": {
Command: {"this", "can", "be", "left"},
Environment: "poc",
},
},
}
I'm not 100% sure I have the interface right, but the API doc for gopkg.in/yaml.v2 doesn't go into detail.
Just had the same problem and my interface looked the same as yours. @TimJones did you find a way to do this?
@augier Sorry this went so long without a reply. Unfortunately I never found a way around this, I simply had to force one format over the other. If you had any better luck I'd be interested in hearing about it!
Took me a while to find the reason...
If you call ReadInConfig() or ReadConfig() on a viper-object, it first parses everything into a map[string]interface{}. However, your structure you want to marshal into is not in play yet, so the used yaml package will blindly parse everything into the map (as your struct types aren't known there), so all UnmarshalYAML (and even UnmarshalJSON) functions will be completely ignored.
If you now unmarshal into your struct, viper uses mapstructure to now map the map[string]interface{} to your structs, but it's not YAML any more.
Ideally mapstructure would provide similar unmarshaling interfaces so this can be handled easily, like
func (m *MyType) UnmarshalMap(value interface{}) error {
// your code ...
}
Imo this is not a bug in viper itself, that's kind of "by design".
(Filed an issue on mapstructure as well to implement such a marshaling interface: https://github.com/mitchellh/mapstructure/issues/115)
Great work @tubocurarin !! This was quite the gotcha for me!!
Most helpful comment
Took me a while to find the reason...
If you call
ReadInConfig()orReadConfig()on a viper-object, it first parses everything into amap[string]interface{}. However, your structure you want to marshal into is not in play yet, so the usedyamlpackage will blindly parse everything into the map (as your struct types aren't known there), so allUnmarshalYAML(and evenUnmarshalJSON) functions will be completely ignored.If you now
unmarshalinto your struct, viper usesmapstructureto now map themap[string]interface{}to your structs, but it's not YAML any more.Ideally
mapstructurewould provide similar unmarshaling interfaces so this can be handled easily, likeImo this is not a bug in viper itself, that's kind of "by design".
(Filed an issue on
mapstructureas well to implement such a marshaling interface: https://github.com/mitchellh/mapstructure/issues/115)