Viper: Nested Environment variable not reflected when fetching parent

Created on 30 Apr 2019  路  4Comments  路  Source: spf13/viper

Consider a config of the following configuration structure

storage:
    redis:
        hostname: "localhost"
        port: 6379
port: 3000

Also consider the following environment variables

STORAGE.REDIS.HOSTNAME=notlocalhost
PORT=3002

When using viper with the above inputs we get inconsistent results when using viper.Get("storage.redis.hostname") and when using viper.Get("storage") as a map[string]interface{}. Whilst the former returns the overridden environment variable notlocalhost, in the latter case the retrieved map ignores the overridden environment variables and pulls up the defaults retrieved from the config.

_Note: Viper version 1.3.2_

Most helpful comment

Hello, I think I have a similar issue, and the SO post does not cover it.

Here is a recreation of my problem, with code example :
I have a config.yaml with :

testkey:
  # one: "hello"
  two: 12345

and a main.go that looks like :

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath("./")

    viper.SetDefault("testkey.one", "bonjour")
    viper.SetDefault("testkey.two", 9876)

    err := viper.ReadInConfig()
    if err != nil {
        panic(err)
    }

    mapResult := viper.Get("testkey")
    fmt.Println(mapResult)

    one := viper.Get("testkey.one")
    fmt.Println(one)
}

I expected it to print map[one:bonjour two:12345], as the key one is commented in the config file, and should therefore have its value set as the default given in the Go code, but I got map[two:12345] instead, where the one key is missing.
However, if I retrieve the value of viper.Get("testkey.one"), I correctly get the default valuebonjour`

To expand a bit on the issue here, I think it would be great if viper.Get'ting a parent, would also look for the default values of its children if necessary, as in the case above.

All 4 comments

You might have run into https://stackoverflow.com/a/2821183

Environment variable names used by the utilities in the Shell [...] consist solely of uppercase letters, digits, and the '_' (underscore)

You could use viper.SetEnvKeyReplacer() to replace all dots to underscores

Hello, I think I have a similar issue, and the SO post does not cover it.

Here is a recreation of my problem, with code example :
I have a config.yaml with :

testkey:
  # one: "hello"
  two: 12345

and a main.go that looks like :

package main

import (
    "fmt"

    "github.com/spf13/viper"
)

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath("./")

    viper.SetDefault("testkey.one", "bonjour")
    viper.SetDefault("testkey.two", 9876)

    err := viper.ReadInConfig()
    if err != nil {
        panic(err)
    }

    mapResult := viper.Get("testkey")
    fmt.Println(mapResult)

    one := viper.Get("testkey.one")
    fmt.Println(one)
}

I expected it to print map[one:bonjour two:12345], as the key one is commented in the config file, and should therefore have its value set as the default given in the Go code, but I got map[two:12345] instead, where the one key is missing.
However, if I retrieve the value of viper.Get("testkey.one"), I correctly get the default valuebonjour`

To expand a bit on the issue here, I think it would be great if viper.Get'ting a parent, would also look for the default values of its children if necessary, as in the case above.

up

@akshaylb
I am not sure if it is a bug, or if actually, Viper does not break down the env var to create a tree.

Like you, I assumed that Viper would overwrite the value loaded from the YAML. But the env name becomes a simple key.

I worked around that by using a function to convert the env vars into JSON, and then loading it into Viper by using viper.MergeConfig:

func envToJson() ([]byte, error) {
    list := map[string]interface{}{}
    for _, e := range os.Environ() {
        pair := strings.SplitN(e, "=", 2)
        indexes := strings.Split(pair[0], "_")
        mountMap(list, indexes, pair[1])
    }
    return json.MarshalIndent(list, "", "    ")
}

func mountMap(m map[string]interface{}, i []string, v interface{}) {
    if len(i) > 1 {
        if _, ok := m[i[0]]; !ok {
            m[i[0]] = map[string]interface{}{}
        }
        asMap, ok := m[i[0]].(map[string]interface{})
        if !ok {
            return // bad configuration
        }
        mountMap(asMap, i[1:], v)
        v = asMap
    }
    m[i[0]] = v
}

envVarAsJson, _ := envToJson()
viper.MergeConfig(bytes.NewBuffer(envVarAsJson))

I only ran few and simple tests, work on common scenarions.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aarondl picture aarondl  路  6Comments

xh3b4sd picture xh3b4sd  路  6Comments

lllama picture lllama  路  3Comments

alfmos picture alfmos  路  3Comments

Giri4joyz picture Giri4joyz  路  3Comments