Preflight Checklist

  • [X] I have searched the issue tracker for an issue that matches the one I want to file, without success.
  • [X] I am not looking for support or already pursued the available support channels without success.
  • [X] I have checked the troubleshooting guide for my problem, without success.

Viper Version

1.19.0

Go Version

1.21

Config Source

Environment variables, Files

Format

INI

Repl.it link

https://replit.com/@jamestrotter4/Go

Code reproducing the issue

package main

import (
    "errors"
    "fmt"
    "log"
    "os"
    "path/filepath"
    "reflect"
    "runtime"
    "strings"

    "github.com/go-playground/validator"
    "github.com/spf13/viper"
)

// config struct

type Config struct {
    Name   string `mapstructure:"name" validate:"required"`
    Nested Nested `mapstructure:"nested"`
}

type Nested struct {
    Val string `mapstructure:"value" validate:"required"`
}

func main() {
    var config Config
    // set env for test
    os.Setenv("NAME", "test")
    os.Setenv("NESTED_VALUE", "test")

    // get the path of the current file
    _, b, _, _ := runtime.Caller(0)
    basepath := filepath.Dir(b)
    err := GetConfig(&config, basepath)
    log.Println(err)

}

func GetConfig(config *Config, cwd string) error {
    if reflect.ValueOf(config).Kind() != reflect.Ptr {
        return errors.New("config must be a pointer")
    }

    //config should be in the same folder that config.go is called from
    viper.AddConfigPath(cwd)
    viper.SetConfigName("config")
    viper.SetConfigType("ini")
    viper.SetEnvKeyReplacer(strings.NewReplacer(`.`, `_`))
    viper.AutomaticEnv()

    // Will not load the config file in development, staging, and production environment. Instead will use configmap

    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); ok {
            log.Println(err)
            log.Println("error loading server env config file (expected)")
        }
    }

    err := viper.Unmarshal(config)
    if err != nil {
        return errors.New("unable to decode into struct")
    }

    // should log values
    log.Println("name: ", config.Name)
    log.Println("nested field: ", config.Nested.Val)

    validate := validator.New()
    if err := validate.Struct(config); err != nil {
        fmt.Print(fmt.Sprintf("%v", err))
        return errors.New("missing required attributes")
    }

    return nil
}

Expected Behavior

Expected to not throw error on validation and for name and nested.val to have a value in logs.

Actual Behavior

config in the end does not have any value, env seems to not be read.

Steps To Reproduce

  1. use version 1.19.0 of viper
  2. use above code (or similar, with AutomaticEnv)
  3. Expect to see no values added

Additional Information

Provided code works with viper version 1.18.0. Ideally I would not want to add BindEnv for every field I want to use. I've tried prefixing the env and other things like that thinking maybe it was missing something. In the end I was not able to get it working.

2024/07/31 12:18:16 Config File "config" Not Found in "[/home/runner/Go]"
2024/07/31 12:18:16 error loading server env config file (expected)

these logs are expected when there is no file. I expect to then get from ENV

Actual

2024/07/31 12:18:16 name:  
2024/07/31 12:18:16 nested field:  
Key: 'Config.Name' Error:Field validation for 'Name' failed on the 'required' tag
Key: 'Config.Nested.Val' Error:Field validation for 'Val' failed on the 'required' tag2024/07/31 12:18:16 missing required attributes

Expected:

2024/07/31 12:18:16 Config File "config" Not Found in "[/home/runner/Go]"
2024/07/31 12:18:16 error loading server env config file (expected)
2024/07/31 12:18:16 name:  test
2024/07/31 12:18:16 nested field:  test

Comment From: github-actions[bot]

👋 Thanks for reporting!

A maintainer will take a look at your issue shortly. 👀

In the meantime: We are working on Viper v2 and we would love to hear your thoughts about what you like or don't like about Viper, so we can improve or fix those issues.

⏰ If you have a couple minutes, please take some time and share your thoughts: https://forms.gle/R6faU74qPRPAzchZ9

📣 If you've already given us your feedback, you can still help by spreading the news, either by sharing the above link or telling people about this on Twitter:

https://twitter.com/sagikazarmark/status/1306904078967074816

Thank you! ❤️

Comment From: tandem97

Same problem. Downgrade to 1.18 solves this problem.

Comment From: vyas-git

Same issue in 1.19 unable to map env to struct.

I checked in changelog after v1.18.2 we need to use viper_bind_struct in tags .

try go run --tags=viper_bind_struct main.go . It works !!

Comment From: parnic

Confirmed, same thing happening here. Starting with version 1.18.2 and continuing into 1.19.0, the only way an environment variable override gets applied in my case is if a config file exists with the target key in it. If there's no config file, or it doesn't have the key I'm trying to override via env, its value isn't being pulled from the env var.

type AlertConfig struct {
    NotificationType string `mapstructure:"NOTIFICATION_TYPE"`
    NotificationURL  string `mapstructure:"NOTIFICATION_URL"`
}

func loadConfig(path string) (config AlertConfig, err error) {
    viper.AddConfigPath(path)
    viper.SetConfigName("alerts")
    viper.SetConfigType("env")

    viper.AutomaticEnv()

    err = viper.ReadInConfig()
    if err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return
        }
    }

    err = viper.Unmarshal(&config)
    return
}

version <1.18.2: whether an alerts.env exists or not, the final value of config.NotificationType is whatever is in the env var. version >= 1.18.2: env var override only works if alerts.env exists containing a NOTIFICATION_TYPE=something entry.

Comment From: florianrusch

I found a workaround to avoid the config file:

// The following is required to introduce the paths to viper. Else viper
// would not be able to fetch these configurations from environment
// variables as it does not know the key to search for. Only configs which
// don't have any default value (`viper.SetDefault`) needs to be mentioned
// here.
var configsWithoutDefaultValues = []byte(`
nested:
  var:
name:
`)
viper.SetConfigType("yaml")
viper.ReadConfig(bytes.NewBuffer(configsWithoutDefaultValues))

Comment From: florianrusch

Or another solution would be to set the default e.g. to an empty string:

viper.SetDefault("nested.var", "")

Comment From: kt1755

I workaround this issue by parsing empty json object config to viper, to init viper key as follow

var cfg Config

viper.SetConfigType("json")

b, err := json.Marshal(cfg)
if err != nil {
    return cfg, err
}

reader := bytes.NewReader(b)
err = viper.ReadConfig(reader)
if err != nil {
    return cfg, err
}
...

Comment From: sadegh-ij

Hey, I tested the following similar scenario, It works without a problem.

Version of used packages:

go v1.23.5 viper v1.20.0 godotenv v1.5.1

Files inside directory: (Assuming you are managing the path of the files correctly)

.env config.yml main.go

main.go

package main

import (
    "fmt"

    "github.com/joho/godotenv"
    "github.com/spf13/viper"
)

type AppConfig struct {
    // hiding crucial information inside a .env file
        // Variables should be visible to viper
    Password  string `mapstructure:"password"`
    SecretKey string `mapstructure:"secret_key"`
}

type Config struct {
    App AppConfig `mapstructure:"app"`
}

func main() {

    // Load environment variables from .env file
    err := godotenv.Load()
    if err != nil {
        log.Fatalf("Error loading .env file")
    }

    // Set the file name and type for Viper
    viper.SetConfigName("config") // name of config file (without extension)
    viper.SetConfigType("yaml")   // required if the config file does not have the extension
    viper.AddConfigPath(".")      // optionally look for config in the working directory

    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

    // This will enable Viper to read environment variables
    viper.AutomaticEnv()

        // You can also set your default vaiables too.
    viper.SetDefault("app.password", "default-password")
    viper.SetDefault("app.secret_key", "default-secret-key")

    app_password := viper.GetString("app.password")
    app_secret_key := viper.GetString("app.secret_key")

    fmt.Println("app->password: ", app_password)
    fmt.Println("app->secret_key: ", app_secret_key)

    // In order to `Unmarshal` viper inside variable `config`,
    // config fields should be visible to viper,
    // so you should name them starting with an Uppercase.
    var config Config
    if err := viper.Unmarshal(&config); err != nil {
        return
        }

    fmt.Println(config)
}

config.yml

app:
  password: ${APP_PASSWORD}
  secret_key: ${APP_SECRET_KEY}

.env

APP_PASSWORD=password
APP_SECRET_KEY=secret-key

Notes

  1. Make sure that your environment variables are set correctly. If you are using a ‍‍‍‍‍‍.env file, ensure that you load it properly using packages like godotenv.
  2. Viper converts the keys in your configuration file to uppercase and replaces dots (.) with underscores (_). So you need to declare your environmental variables by considering this.
  3. Make sure you call viper.AutomaticEnv() before you try to access any configuration values.
  4. (IMPORTANT) In order to call viper.Unmarshal(&config) and assign values loaded by viper inside variable config, config fields should be visible to viper, so you should name them starting with an Uppercase.

Comment From: burik666

@sadegh-ij it works for you because config.yaml is not empty.

Comment From: OliverWangData

Confirming this issue is still present in v1.20.X.

The automatic environment variable mapping configured like this:

v.SetEnvPrefix("PREPEND")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()

When attempting to unmarshal into a struct such as:

type AppConfig struct {
    Proxy struct {
        Url string
    }
}

...
var cfg AppConfig
if err := v.Unmarshal(&cfg); err != nil {
    ....
}
...

Does not correctly populate cfg.Proxy.Url from the PREPEND_PROXY_URL environment variable.

The environment variable is only picked up if an explicit BindEnv call is made, for example:

v.BindEnv("proxy.url", "PREPEND_PROXY_URL")

This workaround defeats the purpose of AutomaticEnv() for struct unmarshaling. Downgrading to v1.18.0 resolves the issue.