goviper-go

How make viper recognise the json in my envvar?


I'm trying to override AllInputChannels with an envvar. The main config is in config.yaml which is picked up fine.

Running the below code viper.Unmarshal() throws an error:

'AllInputChannels' expected a map, got 'string'

How should I present the value in the envvar so it's recognised?

I've removed error checking code below for clarity, except where the error appears.

type ChannelMap struct {
    Team    string `mapstructure:"Team"`
    Channel string `mapstructure:"Channel"`
}

type AllInputChannels struct {
    Production []ChannelMap `mapstructure:"Production"`
    Staging    []ChannelMap `mapstructure:"Staging"`
}

type Config struct {
    AllInputChannels AllInputChannels `mapstructure:"AllInputChannels"`
}

func NewConfig() Config { return Config{} }

func Build() Config {
    conf := NewConfig()
    
    _ = os.Setenv("ALLINPUTCHANNELS", `{"staging":[{"team":"Foo","channel":"DEF456"}]}`)

    viper.AddConfigPath(".") // Local builds.
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")

    _ = viper.BindEnv("AllInputChannels")

    _ = viper.ReadInConfig()

    err := viper.Unmarshal(&conf)
    if err != nil {
        fmt.Printf("unable to decode config, %v", err)
    }

    return conf, nil
}

func main() {
    _ = Build()
}

Solution

  • package main
    
    import (
        "encoding/json"
        "fmt"
        "os"
        "github.com/mitchellh/mapstructure"
        "github.com/spf13/viper"
    )
    
    type ChannelMap struct {
        Team    string `mapstructure:"team"`
        Channel string `mapstructure:"channel"`
    }
    
    type AllInputChannels struct {
        Production []ChannelMap `mapstructure:"production"`
        Staging    []ChannelMap `mapstructure:"staging"`
    }
    
    type Config struct {
        AllInputChannels AllInputChannels `mapstructure:"AllInputChannels"`
    }
    
    func NewConfig() Config { return Config{} }
    
    func Build() Config {
        conf := NewConfig()
    
        _ = os.Setenv("ALLINPUTCHANNELS", `{"staging":[{"team":"Foo","channel":"DEF456"}]}`)
        viper.AutomaticEnv()
    
        viper.AddConfigPath(".") // Local builds.
        viper.SetConfigName("config")
        viper.SetConfigType("yaml")
    
        
        // Load the environment variable and unmarshal the JSON value into the struct
    
        if envValue := viper.GetString("ALLINPUTCHANNELS"); envValue != "" {
    
            var allInputChannels AllInputChannels
    
            err := json.Unmarshal([]byte(envValue), &allInputChannels)
    
            if err != nil {
    
                fmt.Printf("Error unmarshaling JSON from environment variable: %v\n", err)
    
                return conf
    
            }
    
            conf.AllInputChannels = allInputChannels
    
        }
    
        _ = viper.ReadInConfig()
    
        err := viper.Unmarshal(&conf, func(c *mapstructure.DecoderConfig) {
    
            c.TagName = "mapstructure"
    
        })
        if err != nil {
            fmt.Printf("unable to decode config, %v", err)
        }
    
        return conf
    }
    
    func main() {
        conf := Build()
    
        fmt.Printf("Config: %+v\n", conf)
    }
    

    changes made :-

    1. The mapstructure tags are case-sensitive and must match the keys in the JSON data exactly.
    2. The os.Setenv("ALLINPUTCHANNELS", ...) sets the environment variable correctly.
    3. viper.AutomaticEnv() ensures Viper reads environment variables automatically.
    4. After loading the environment variable ALLINPUTCHANNELS, its value is parsed as JSON using json.Unmarshal into the AllInputChannels struct.
    5. The AllInputChannels field in the conf struct is then set to the parsed value.
    6. Returning conf instead conf,nil where only config required as return value