Repository: spf13/viper (go)

Issue:
Default decode hook for mapstructure
### Preflight Checklist

- [X] I have searched the [issue tracker](https://www.github.com/spf13/viper/issues) for an issue that matches the one I want to file, without success.

### Problem Description

Unmarshal requires custom decoding hooks to be passed to it.

### Proposed Solution

Allow configuring a default decode hook in `NewWithOptions`.

### Alternatives Considered

_No response_

### Additional Information

_No response_

Pull request:
Add decoder hook option
Allow setting a custom, default decode hook.

Fixes #1606

Patch:
diff --git a/encoding.go b/encoding.go
index 52511db..a7da558 100644
--- a/encoding.go
+++ b/encoding.go
@@ -58,6 +58,10 @@ type CodecRegistry interface {
 // WithEncoderRegistry sets a custom [EncoderRegistry].
 func WithEncoderRegistry(r EncoderRegistry) Option {
 	return optionFunc(func(v *Viper) {
+		if r == nil {
+			return
+		}
+
 		v.encoderRegistry = r
 	})
 }
@@ -65,6 +69,10 @@ func WithEncoderRegistry(r EncoderRegistry) Option {
 // WithDecoderRegistry sets a custom [DecoderRegistry].
 func WithDecoderRegistry(r DecoderRegistry) Option {
 	return optionFunc(func(v *Viper) {
+		if r == nil {
+			return
+		}
+
 		v.decoderRegistry = r
 	})
 }
@@ -72,6 +80,10 @@ func WithDecoderRegistry(r DecoderRegistry) Option {
 // WithCodecRegistry sets a custom [EncoderRegistry] and [DecoderRegistry].
 func WithCodecRegistry(r CodecRegistry) Option {
 	return optionFunc(func(v *Viper) {
+		if r == nil {
+			return
+		}
+
 		v.encoderRegistry = r
 		v.decoderRegistry = r
 	})
diff --git a/finder.go b/finder.go
index ab4b595..9b203ea 100644
--- a/finder.go
+++ b/finder.go
@@ -9,6 +9,10 @@ import (
 // WithFinder sets a custom [Finder].
 func WithFinder(f Finder) Option {
 	return optionFunc(func(v *Viper) {
+		if f == nil {
+			return
+		}
+
 		v.finder = f
 	})
 }
@@ -34,6 +38,10 @@ func (c *combinedFinder) Find(fsys afero.Fs) ([]string, error) {
 	var errs []error
 
 	for _, finder := range c.finders {
+		if finder == nil {
+			continue
+		}
+
 		r, err := finder.Find(fsys)
 		if err != nil {
 			errs = append(errs, err)
diff --git a/viper.go b/viper.go
index 6ec598c..d2f85d8 100644
--- a/viper.go
+++ b/viper.go
@@ -183,6 +183,8 @@ type Viper struct {
 	encoderRegistry EncoderRegistry
 	decoderRegistry DecoderRegistry
 
+	decodeHook mapstructure.DecodeHookFunc
+
 	experimentalFinder     bool
 	experimentalBindStruct bool
 }
@@ -247,10 +249,25 @@ type StringReplacer interface {
 // EnvKeyReplacer sets a replacer used for mapping environment variables to internal keys.
 func EnvKeyReplacer(r StringReplacer) Option {
 	return optionFunc(func(v *Viper) {
+		if r == nil {
+			return
+		}
+
 		v.envKeyReplacer = r
 	})
 }
 
+// WithDecodeHook sets a default decode hook for mapstructure.
+func WithDecodeHook(h mapstructure.DecodeHookFunc) Option {
+	return optionFunc(func(v *Viper) {
+		if h == nil {
+			return
+		}
+
+		v.decodeHook = h
+	})
+}
+
 // NewWithOptions creates a new Viper instance.
 func NewWithOptions(opts ...Option) *Viper {
 	v := New()
@@ -896,7 +913,7 @@ func UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
 }
 
 func (v *Viper) UnmarshalKey(key string, rawVal any, opts ...DecoderConfigOption) error {
-	return decode(v.Get(key), defaultDecoderConfig(rawVal, opts...))
+	return decode(v.Get(key), v.defaultDecoderConfig(rawVal, opts...))
 }
 
 // Unmarshal unmarshals the config into a Struct. Make sure that the tags
@@ -919,13 +936,13 @@ func (v *Viper) Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
 	}
 
 	// TODO: struct keys should be enough?
-	return decode(v.getSettings(keys), defaultDecoderConfig(rawVal, opts...))
+	return decode(v.getSettings(keys), v.defaultDecoderConfig(rawVal, opts...))
 }
 
 func (v *Viper) decodeStructKeys(input any, opts ...DecoderConfigOption) ([]string, error) {
 	var structKeyMap map[string]any
 
-	err := decode(input, defaultDecoderConfig(&structKeyMap, opts...))
+	err := decode(input, v.defaultDecoderConfig(&structKeyMap, opts...))
 	if err != nil {
 		return nil, err
 	}
@@ -942,20 +959,29 @@ func (v *Viper) decodeStructKeys(input any, opts ...DecoderConfigOption) ([]stri
 
 // defaultDecoderConfig returns default mapstructure.DecoderConfig with support
 // of time.Duration values & string slices.
-func defaultDecoderConfig(output any, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
-	c := &mapstructure.DecoderConfig{
-		Metadata:         nil,
-		Result:           output,
-		WeaklyTypedInput: true,
-		DecodeHook: mapstructure.ComposeDecodeHookFunc(
+func (v *Viper) defaultDecoderConfig(output any, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
+	decodeHook := v.decodeHook
+	if decodeHook == nil {
+		decodeHook = mapstructure.ComposeDecodeHookFunc(
 			mapstructure.StringToTimeDurationHookFunc(),
 			// mapstructure.StringToSliceHookFunc(","),
 			stringToWeakSliceHookFunc(","),
-		),
+		)
 	}
+
+	c := &mapstructure.DecoderConfig{
+		Metadata:         nil,
+		WeaklyTypedInput: true,
+		DecodeHook:       decodeHook,
+	}
+
 	for _, opt := range opts {
 		opt(c)
 	}
+
+	// Do not allow overwriting the output
+	c.Result = output
+
 	return c
 }
 
@@ -997,7 +1023,7 @@ func UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
 }
 
 func (v *Viper) UnmarshalExact(rawVal any, opts ...DecoderConfigOption) error {
-	config := defaultDecoderConfig(rawVal, opts...)
+	config := v.defaultDecoderConfig(rawVal, opts...)
 	config.ErrorUnused = true
 
 	keys := v.AllKeys()


Test patch:
diff --git a/viper_test.go b/viper_test.go
index a4df402..1419113 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -894,6 +894,41 @@ func TestUnmarshal(t *testing.T) {
 	)
 }
 
+func TestUnmarshalWithDefaultDecodeHook(t *testing.T) {
+	opt := mapstructure.ComposeDecodeHookFunc(
+		mapstructure.StringToTimeDurationHookFunc(),
+		mapstructure.StringToSliceHookFunc(","),
+		// Custom Decode Hook Function
+		func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) {
+			if rf != reflect.String || rt != reflect.Map {
+				return data, nil
+			}
+			m := map[string]string{}
+			raw := data.(string)
+			if raw == "" {
+				return m, nil
+			}
+			err := json.Unmarshal([]byte(raw), &m)
+			return m, err
+		},
+	)
+
+	v := NewWithOptions(WithDecodeHook(opt))
+	v.Set("credentials", "{\"foo\":\"bar\"}")
+
+	type config struct {
+		Credentials map[string]string
+	}
+
+	var C config
+
+	require.NoError(t, v.Unmarshal(&C), "unable to decode into struct")
+
+	assert.Equal(t, &config{
+		Credentials: map[string]string{"foo": "bar"},
+	}, &C)
+}
+
 func TestUnmarshalWithDecoderOptions(t *testing.T) {
 	v := New()
 	v.Set("credentials", "{\"foo\":\"bar\"}")