I have the following YAML structure:
type Pipeline struct {
Name string `yaml:"name"`
Nodes map[string]NodeConfig `yaml:"nodes"`
Connections []NodeConnection `yaml:"connections"`
}
type NodeConfig struct {
Type string `yaml:"type"`
Config interface{} `yaml:"config"`
}
For each NodeConfig
, depending on the value of Type
, I need to detect the real type of Config
.
switch nc.Type {
case "request":
return NewRequestNode(net, name, nc.Config.(RequestConfig))
case "log":
return NewLogNode(net, name)
//...
}
This is the error I get from this:
panic: interface conversion: interface {} is map[string]interface {}, not main.RequestConfig
I suspect it's because Config
is getting automatically recognized as a map[string]interface{}
, when I really want it to just be an interface{}
. How can I do this?
Edit: Minimal Example
CodePudding user response:
You are correct about the problem, it is getting automatically recognized as a map[string]interface{}
, since you don't provide a custom UnmarshalYAML func the YAML package can only do that. But you actually don't want it as just interface{}
, you need to identify which actual implementation you want for that.
Solution using yaml.v3
I don't see how you can solve it without providing a custom UnmarshalYAML
func to NodeConfig
type. If that was JSON, I would read the Config
as a json.RawMessage
, then for each possible type I would unmarshal it into the desired type, and yaml.v3 equivalent seems to be a yaml.Node type.
Using this, you can create a struct similar to NodeConfig
which has the Config
as yaml.Node
and convert it to the concrete type based on the Type
value, like this:
func (nc *NodeConfig) UnmarshalYAML(value *yaml.Node) error {
var ncu struct {
Type string `yaml:"type"`
Config yaml.Node `yaml:"config"`
}
var err error
// unmarshall into a NodeConfigUnmarshaler to detect correct type
err = value.Decode(&ncu)
if err != nil {
return err
}
// now, detect the type and covert it accordingly
nc.Type = ncu.Type
switch ncu.Type {
case "request":
nc.Config = &RequestConfig{}
case "log":
nc.Config = &LogConfig{}
default:
return fmt.Errorf("unknown type %q", ncu.Type)
}
err = ncu.Config.Decode(nc.Config)
return err
}
Sample code
To test that, I created dummies RequestConfig
and LogConfig
and a sample:
type RequestConfig struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
type LogConfig struct {
Message string `yaml:"message"`
}
func main() {
logSampleYAML := []byte(`
type: log
config:
message: this is a log message
`)
reqSampleYAML := []byte(`
type: request
config:
foo: foo value
bar: bar value
`)
for i, val := range [][]byte{logSampleYAML, reqSampleYAML} {
var nc NodeConfig
err := yaml.Unmarshal(val, &nc)
if err != nil {
fmt.Printf("failed to parse sample %d: %v\n", i, err)
} else {
fmt.Printf("sample %d type %q (%T) = % v\n", i, nc.Type, nc.Config, nc.Config)
}
}
}
Which outputs:
sample 0 type "log" (*main.LogConfig) = &{Message:this is a log message}
sample 1 type "request" (*main.RequestConfig) = &{Foo:foo value Bar:bar value}
So, as you can see each instance of NodeConfig
is instanciating the Config
with the concrete type required, which means you can now use type assertion as Confg.(*RequestConfig)
or Config.(*LogConfig)
(or switch
, of course).
You can play with that solution in this Go Playground full sample.
Solution using yaml.v2
I have made a mistake and sent a solution with v2, but I recommend anyone to use the v3. If you can't, follow the v2 version...
The v2 does not have yaml.Node
, but I found a very similar solution in the answer of this issue (I fixed a typo there):
type RawMessage struct {
unmarshal func(interface{}) error
}
func (msg *RawMessage) UnmarshalYAML(unmarshal func(interface{}) error) error {
msg.unmarshal = unmarshal
return nil
}
func (msg *RawMessage) Unmarshal(v interface{}) error {
return msg.unmarshal(v)
}
Which is an interesting trick, and with that you could bake your own UnmarshalYAML
func by loading it into a temporary struct and then identifying each type you want and without needing to process the YAML twice:
func (nc *NodeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
var ncu struct {
Type string `yaml:"type"`
Config RawMessage `yaml:"config"`
}
var err error
// unmarshall into a NodeConfigUnmarshaler to detect correct type
err = unmarshal(&ncu)
if err != nil {
return err
}
// now, detect the type and covert it accordingly
nc.Type = ncu.Type
switch ncu.Type {
case "request":
cfg := &RequestConfig{}
err = ncu.Config.Unmarshal(cfg)
nc.Config = cfg
case "log":
cfg := &LogConfig{}
err = ncu.Config.Unmarshal(cfg)
nc.Config = cfg
default:
return fmt.Errorf("unknown type %q", ncu.Type)
}
return err
}
The sample code for v2 and v3 are identical.
You can play with that solution in this Go Playground full sample.