Home > Software design >  Force unmarshal as interface{} instead of map[string]interface{}
Force unmarshal as interface{} instead of map[string]interface{}

Time:10-07

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.

  •  Tags:  
  • go
  • Related