Home > other >  Unmarshaling YAML into different struct based off YAML field
Unmarshaling YAML into different struct based off YAML field

Time:01-09

I'm trying to unmarshal the following YAML data into Go structures.

The data is the in the following format:

fetchers:
- type: "aws"
  config:
    omega: "lul"
- type: "kubernetes"
  config: 
    foo: "bar"

Based of the type field, I want to determine wether to unmarshal the config field into awsConfig or kubernetesConfig struct.

My current code looks like this (using "gopkg.in/yaml.v2"):

type kubernetesConfig struct {
    foo string `yaml:"foo"`
}
type awsConfig struct {
    omega string `yaml:"omega"`
}

var c struct {
    Fetchers []struct {
        Type   string      `yaml:"type"`
        Config interface{} `yaml:"config"`
    } `yaml:"fetchers"`
}

err := yaml.Unmarshal(data, &c)
if err != nil {
    log.Fatal(err)
}
for _, val := range c.Fetchers {
    switch val.Type {
    case "kubernetes":
        conf := val.Config.(kubernetesConfig)
        fmt.Println(conf.foo)
    case "aws":
        conf := val.Config.(awsConfig)
        fmt.Println(conf.omega)
    default:
        log.Fatalf("No matching type, was type %v", val.Type)
    }
}

Code in playground: https://go.dev/play/p/klxOoHMCtnG

Currently it gets unmarshalled as map[interface {}]interface {}, which can't be converted to one of the structs above.
Error: panic: interface conversion: interface {} is map[interface {}]interface {}, not main.awsConfig \

Do I have to implemented the Unmarshaler Interface of the YAML package with a custom UnmarshalYAML function to get this done?

CodePudding user response:

Found the solution by implementing Unmarshaler Interface:

type Fetcher struct {
    Type   string      `yaml:"type"`
    Config interface{} `yaml:"config"`
}

// Interface compliance
var _ yaml.Unmarshaler = &Fetcher{}

func (f *Fetcher) UnmarshalYAML(unmarshal func(interface{}) error) error {
    var t struct {
        Type string `yaml:"type"`
    }
    err := unmarshal(&t)
    if err != nil {
        return err
    }
    f.Type = t.Type
    switch t.Type {
    case "kubernetes":
        var c struct {
            Config kubernetesConfig `yaml:"config"`
        }
        err := unmarshal(&c)
        if err != nil {
            return err
        }
        f.Config = c.Config
    case "aws":
        var c struct {
            Config awsConfig `yaml:"config"`
        }
        err := unmarshal(&c)
        if err != nil {
            return err
        }
        f.Config = c.Config
    }
    return nil
}

CodePudding user response:

This type of task - where you want to delay the unmarshaling - is very similar to how json.RawMessage works with examples like this.

The yaml package does not have a similar mechanism for RawMessage - but this technique can easily be replicated as outlined here:

type RawMessage struct {
    unmarshal func(interface{}) error
}

func (msg *RawMessage) UnmarshalYAML(unmarshal func(interface{}) error) error {
    msg.unmarshal = unmarshal
    return nil
}

// call this method later - when we know what concrete type to use
func (msg *RawMessage) Unmarshal(v interace{}) error {
    return msg.unmarshal(v)
}

So to leverage this in your case:

var fs struct {
    Configs []struct {
        Type   string     `yaml:"type"`
        Config RawMessage `yaml:"config"` // delay unmarshaling
    } `yaml:"fetchers"`
}

err = yaml.Unmarshal([]byte(data), &fs)
if err != nil {
    return
}

and based on the config "Type" (aws or kubernetes), you can finally unmarshal the RawMessage into the correct concrete type:

aws := awsConfig{} // concrete type
err = c.Config.Unmarshal(&aws)

or:

k8s := kubernetesConfig{} // concrete type
err = c.Config.Unmarshal(&k8s)

Working example here: https://go.dev/play/p/wsykOXNWk3H

  •  Tags:  
  • Related