I noticed an issue where viper.Sub() fails to extract default values of a nested key. I attached demo code to reproduce the issue using latest viper and Go 1.12.6 under x64_linux.

main.go:

package main

import (
    "fmt"

    "github.com/davecgh/go-spew/spew"
    "github.com/spf13/viper"
)

func main() {
    viper.SetDefault("config.value2.internal", 3)
    viper.SetConfigFile("./demo.yaml")
    err := viper.ReadInConfig()
    if err != nil {
        panic(fmt.Errorf("Fatal error config file: %s ", err))
    }
    // Until now, everything works as expected
    fmt.Printf("config.value1: %d\n", viper.GetInt("config.value1"))
    fmt.Printf("config.value2: %d\n", viper.GetInt("config.value2"))
    fmt.Printf("config.value2.internal: %d\n", viper.GetInt("config.value2.internal"))

    // Extract the config subtree
    v := viper.Sub("config")
    // This doesn't work if value2 node is missing in configuration
    fmt.Printf("value2.internal: %d\n", v.GetInt("value2.internal"))

    spew.Dump(v.AllSettings())
}

demo.yaml (works as inteded, but defaults are useless since everything is explicitly set):

config:
  value1: 1
  value2: 
    internal: 3

demo.yaml (broken):

config:
  value1: 1
  #value2: 
  #  internal: 3

Output for the broken example:

config.value1: 1
config.value2: 0
config.value2.internal: 3
value2.internal: 0
(map[string]interface {}) (len=1) {
 (string) (len=6) "value1": (int) 1
}

Whereas I would expect to have value2.internal: 3

The issue seems somehow related / similar to issue #71 as well as PR #195

Comment From: aquincum

pretty impressive this hasn't been fixed in 3 years and it seems like it doesn't impact many...

Comment From: setrofim

I ran into related issue when Get()'ing a sub-tree that defaults specified for some of its keys. The issue is that when a value is specified for a path in the config, it effectively shadows the corresponding path in the defaults. But if the nested deeper path is not present in the set value, path resolution falls back to looking in the defaults. This results in potentially inconsistent behaviour when accessing a configuration point via its full path results in one value (the default), while accessing it via a relative path from a sub-tree results an that type's null value (nil, 0, "", etc).

Potential fix in https://github.com/spf13/viper/pull/1439

(note: this technically changes existing expected behaviour -- see the updated overrides_test.go; however, given this issue, and the referenced previous discussions, merging nested keys from defaults seems like desirable behaviour?)

Comment From: dp1140a

Well Im late to the party but this bit me in the ass. I have a function that sets defaults:

func (lc *LoggerConfig) RegisterDefaults(v *viper.Viper) {
    v.SetDefault("logging.loglevel", "INFO")
    v.SetDefault("logging.logFile", fmt.Sprintf("log/%v.log", pkg.APP_NAME))
    v.SetDefault("logging.stdOut", true)
    v.SetDefault("logging.fileOut", false)
    v.SetDefault("logging.format", "json")
    // no defaults for id/name here (they’re dynamic)
}

But at runtime I want these overriden from a toml config file.

[logging]
format="text"
loglevel="DEBUG"

That works . . . Somewhat. I can see the proper settings from my config file in Viper.config and I can see my defaults in Viper.defaults and if I do Viper.AllSettings() it seems to merge them:

logging:map[fileout:false format:text logfile:log/swarmos.log loglevel:DEBUG stdout:true]

But if I try Viper.Sub("logging"):

map[format:text loglevel:DEBUG]

And Viper.Unmarshal:

type LoggerConfig struct {
    LogFile  string `json:"logFile" toml:"logFile"`
    LogLevel string `mapstructure:"loglevel" json:"loglevel" toml:"loglevel"`
    StdOut   bool   `json:"stdOut" toml:"stdOut"`
    FileOut  bool   `json:"fileOut" toml:"fileOut"`
    Format   string `json:"format" toml:"format"`
}

cfg := &LoggerConfig{}
sub := viper.Sub(_MODULE)
if sub == nil {
    // If this happens, something is off with key names
    log.Error("viper.Sub(\"logging\") returned nil")
    return cfg
}
if err := sub.Unmarshal(cfg); err != nil {
    log.WithError(err).Error("failed to unmarshal logger config")
}

And then print cfg:

{"stdOut":true,"fileOut":false,"format":""}