Home > Blockchain >  Unmarshal YAML into complex object which may be either struct or string
Unmarshal YAML into complex object which may be either struct or string

Time:01-27

Trying to unmarshal YAML into complex object such as map[string]map[interface{}]string. The problem is that I want to be able to differentiate an interface{} part between string and Source which is a struct.

type Source struct {
    ID     string `yaml:"id"`
    Name   string `yaml:"name"`
    LogoID string `yaml:"logoId"`
    URL    string `yaml:"url"`
}

type UNFT struct {
    ItemMeta map[string]map[interface{}]string `yaml:"item_meta"`
    // could be
    // ItemMeta map[string]map[string]string `yaml:"item_meta"`
    // or
    // ItemMeta map[string]map[Source]string `yaml:"item_meta"`
}

Obviously YAML does not know how to unmarshal into Source struct so I have to implement Unmarshaler interface:

type Unmarshaler interface {
    UnmarshalYAML(value *Node) error
}

But I don't quite understand the big picture of unmarshaling process. In general I assume that I have to manually traverse *yaml.Node and call func UnmarshalYAML(value *Node) error on every node.

package main

import (
    "fmt"

    "gopkg.in/yaml.v3"
)

type Source struct {
    ID     string `json:"id"`
    Name   string `json:"name"`
    LogoID string `json:"logoId"`
    URL    string `json:"url"`
}

var data = `
unf:
    item_meta:
      source:
           !struct
           ? id: "data-watch" 
             name: "DataWatch"
             logoid: "data-watch"
             url: "https"
           : "product_any('SS')"
      public_usage:
        "": "source_any('SDF')"
        "provider": "source_any('ANO')"`

type UNFT struct {
    ItemMeta map[string]map[interface{}]string `yaml:"item_meta"`
}

type MetaConverterConfigT struct {
    UNFT UNFT `yaml:"unf"`
}

func main() {
    cfg := MetaConverterConfigT{}

    err := yaml.Unmarshal([]byte(data), &cfg)
    if err != nil {
        fmt.Println("%w", err)
    }

    fmt.Println(cfg)
}

func (s *UNFT) UnmarshalYAML(n *yaml.Node) error {
    var cfg map[string]map[interface{}]string
    if err := n.Decode(&cfg); err != nil {
        fmt.Println("%w", err)
    }

    return nil
}

Go playground

CodePudding user response:

type MetaKey struct {
    String string
    Source Source
}

func (k *MetaKey) UnmarshalYAML(n *yaml.Node) error {
    if n.Tag == "!!str" {
        return n.Decode(&k.String)
    }
    if n.Tag == "!!map" {
        return n.Decode(&k.Source)
    }
    return fmt.Errorf("unsupported MetaKey type")
}

// ...

type UNFT struct {
    ItemMeta map[string]map[MetaKey]string `yaml:"item_meta"`
}

https://go.dev/play/p/Nhtab4l-ANT


If you need the map type to remain as is, i.e. without adding the custom key type, then you can implement the unmarshaler on UNFT as well and just do a re-mapping with any:

type UNFT struct {
    ItemMeta map[string]map[any]string `yaml:"item_meta"`
}

func (u *UNFT) UnmarshalYAML(n *yaml.Node) error {
    var obj struct {
        ItemMeta map[string]map[MetaKey]string `yaml:"item_meta"`
    }
    if err := n.Decode(&obj); err != nil {
        return err
    }

    u.ItemMeta = make(map[string]map[any]string, len(obj.ItemMeta))
    for k, v := range obj.ItemMeta {
        m := make(map[any]string, len(v))
        for k, v := range v {
            if k.Source != (Source{}) {
                m[k.Source] = v
            } else {
                m[k.String] = v
            }
        }
        u.ItemMeta[k] = m
    }
    return nil
}

https://go.dev/play/p/uwboGKf3qnD

  • Related