How do I use Viper to get a value from a nested YAML structure?

22.3k views Asked by At

How do I write the code below to get a string from my nested yaml struct?

Here is my yaml:

element:
  - one:
      url: http://test
      nested: 123
  - two:
      url: http://test
      nested: 123

weather:
  - test:
      zipcode: 12345
  - ca:
      zipcode: 90210

Here is example code

viper.SetConfigName("main_config")
  viper.AddConfigPath(".")
  err := viper.ReadInConfig()
  if err != nil {
    panic(err)
  }
testvar := viper.GetString("element.one.url")

My problem:

I get a blank string when I print this. According to the docs, this is how you get a nested element. I suspect its not working because the elements are lists. Do I need to do a struct? I am not sure how to make one, especially if it needs to be nested.

4

There are 4 answers

1
Deepak Singh On

There are different Get methods available in viper library and your YML structure is of type []map[string]string, so to parse your YML configuration file you have to use viper.Get method. So you have to parse your file something like this..

Note: You can use struct as well to un-marshal the data. Please refer this post mapping-nested-config-yaml-to-struct

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        panic(err)
    }
    testvar := viper.Get("element")
    fmt.Println(testvar)
    elementsMap := testvar.([]interface{})
    for k, vmap := range elementsMap {
        fmt.Print("Key: ", k) 
        fmt.Println(" Value: ", vmap)
        eachElementsMap := vmap.(map[interface{}]interface{})

        for k, vEachValMap := range eachElementsMap {
            fmt.Printf("%v: %v \n", k, vEachValMap)
            vEachValDataMap := vEachValMap.(map[interface{}]interface{})

            for k, v := range vEachValDataMap {
                fmt.Printf("%v: %v \n", k, v)
            }
        }
    }
}

// Output:
/*
Key: 0 Value:  map[one:map[url:http://test nested:123]]
one: map[url:http://test nested:123]
url: http://test
nested: 123
Key: 1 Value:  map[two:map[url:http://test nested:123]]
two: map[url:http://test nested:123]
url: http://test
nested: 123
*/
0
samad montazeri On

You can use Unmarshal or UnmarshalKey to parse all or part of your data and fill a struct. It is very similar to unmarshaling a json.

In your case, code will be like this:

package main

import (
    "fmt"
    "github.com/spf13/viper"
)

// here we define schema of data, just like what we might do when we parse json
type Element struct {
    Url    string `mapstructure:"url"`
    Nested int    `mapstructure:"nested"`
}

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        panic(err)
    }
    
    // data in `element` key is a map of string to Element. We define a variable to store data into it.
    elementParsed := make(map[string]*Element)
    // read the key `element` in the yaml file, and parse it's data and put it in `elementParsed` variable
    err = viper.UnmarshalKey("element", &elementParsed)
    if err != nil {
        panic(err)
    }

    fmt.Println(elementParsed["one"].Url) // will print: http://test 
    fmt.Println(elementParsed["one"].Nested) // will print: 123
}
0
webcpu On

You can unmarshal a nested configuration file.

main.go

package main

import (
    "fmt"
    "github.com/spf13/viper"
)

type NestedURL struct {
    URL string `mapstructure:"url"`
    Nested int `mapstructure:"nested"`
}

type ZipCode struct {
    Zipcode string `mapstructure:"zipcode"`
}

type Config struct {
    Element [] map[string]NestedURL `mapstructure:"element"`
    Weather [] map[string]ZipCode `mapstructure:"weather"`
}

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    if err := viper.ReadInConfig();  err != nil {
        return
    }
    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(config)
}

config.yml

element:
  - one:
      url: http://test
      nested: 123
  - two:
      url: http://test
      nested: 123
weather:
  - test:
      zipcode: 12345
  - ca:
      zipcode: 90210
1
xzeu On
config.go
package config

import (
    "fmt"

    "github.com/fsnotify/fsnotify"
    "github.com/spf13/viper"
)

type User struct {
    Username string `mapstructure:"username"`
    Passwd   string `mapstructure:"passwd"`
}

type Config struct {
    Account []User `mapstructure:"account"`
}

var AppConfig Config

func init() {
    viperCfg := viper.New()

    viperCfg.SetConfigName("config")
    viperCfg.SetConfigType("yaml")
    viperCfg.AddConfigPath("../")

    err := viperCfg.ReadInConfig()
    if err != nil {
        panic(err)
    }
    // fmt.Println(viperCfg)
    err = viperCfg.Unmarshal(&AppConfig)
    if err != nil {
        panic(err)
    }

    viperCfg.WatchConfig()
    viperCfg.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("Config file changed:", e.Name)
        if err = viperCfg.Unmarshal(&AppConfig); err != nil {
            fmt.Println(err)
        }
    })
}

func GetConfig() Config {
    return AppConfig
}
account:
  - username: abc
    passwd: 123
  - username: def
    passwd: 123
config_test.go
package config

import (
    "testing"
)

func TestConfig(t *testing.T) {
    config := AppConfig
    t.Log("username:", config.Account[0].Username)
    t.Log("passwd:", config.Account[0].Passwd)
}

result

Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestConfig$ goask/config

=== RUN   TestConfig
    /Users/xzeu/go/src/digask/config/config_test.go:9: username: abc
    /Users/xzeu/go/src/digask/config/config_test.go:10: passwd: 123
--- PASS: TestConfig (0.00s)
PASS
ok      goask/config    0.968s