From dcd7fbbff759c1a4465ec97971ab48b100af7b98 Mon Sep 17 00:00:00 2001
From: Bogdan Drutu <bogdandrutu@gmail.com>
Date: Fri, 18 Sep 2020 10:08:05 -0700
Subject: [PATCH] Allow nil values to be returned by AllSettings

Signed-off-by: Bogdan Drutu <bogdandrutu@gmail.com>
---
 viper.go      | 14 +++++++++++---
 viper_test.go | 31 +++++++++++++++++++++++++++++++
 2 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/viper.go b/viper.go
index c3130c2..4d5b02f 100644
--- a/viper.go
+++ b/viper.go
@@ -199,6 +199,7 @@ type Viper struct {
 	automaticEnvApplied bool
 	envKeyReplacer      StringReplacer
 	allowEmptyEnv       bool
+	allowNilValues      bool
 
 	config         map[string]interface{}
 	override       map[string]interface{}
@@ -257,6 +258,14 @@ func KeyDelimiter(d string) Option {
 	})
 }
 
+// AllowNilValues tells Viper to not ignore keys with nil values.
+// For backward compatibility reasons this is false by default.
+func AllowNilValues(allowNilValues bool) Option {
+	return optionFunc(func(v *Viper) {
+		v.allowNilValues = allowNilValues
+	})
+}
+
 // StringReplacer applies a set of replacements to a string.
 type StringReplacer interface {
 	// Replace returns a copy of s with all replacements performed.
@@ -1960,9 +1969,8 @@ func (v *Viper) AllSettings() map[string]interface{} {
 	// start from the list of keys, and construct the map one value at a time
 	for _, k := range v.AllKeys() {
 		value := v.Get(k)
-		if value == nil {
-			// should not happen, since AllKeys() returns only keys holding a value,
-			// check just in case anything changes
+		// Default behavior is to not allow nil values, but users can enable this via config.
+		if value == nil && !v.allowNilValues {
 			continue
 		}
 		path := strings.Split(k, v.keyDelim)
diff --git a/viper_test.go b/viper_test.go
index 09d5021..66fe457 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -2258,6 +2258,37 @@ func TestKeyDelimiter(t *testing.T) {
 	assert.Equal(t, expected, actual)
 }
 
+var yamlExampleWithNilValues = []byte(`Hacker: true
+name: steve
+hobbies:
+clothing:
+  jacket:
+`)
+
+func TestNilValues(t *testing.T) {
+	vNil := NewWithOptions(AllowNilValues(true))
+	vNil.SetConfigType("yaml")
+	require.NoError(t, vNil.ReadConfig(strings.NewReader(string(yamlExampleWithNilValues))))
+	expectedNil := map[string]interface{}{
+		"hacker":  true,
+		"name":    "steve",
+		"hobbies": nil,
+		"clothing": map[string]interface{}{
+			"jacket": nil,
+		},
+	}
+	assert.EqualValues(t, expectedNil, vNil.AllSettings())
+
+	vNotNil := NewWithOptions(AllowNilValues(false))
+	vNotNil.SetConfigType("yaml")
+	require.NoError(t, vNotNil.ReadConfig(strings.NewReader(string(yamlExampleWithNilValues))))
+	expectedNotNil := map[string]interface{}{
+		"hacker": true,
+		"name":   "steve",
+	}
+	assert.EqualValues(t, expectedNotNil, vNotNil.AllSettings())
+}
+
 func BenchmarkGetBool(b *testing.B) {
 	key := "BenchmarkGetBool"
 	v = New()