Skip to content

Commit 41eafd1

Browse files
Merge pull request #167 from go-viper/fix-nil-hook
fix: nil value in decode hooks
2 parents 9aa3f77 + cd52f89 commit 41eafd1

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

decode_hooks.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ import (
1313
"time"
1414
)
1515

16+
// safeInterface safely extracts the interface value from a reflect.Value.
17+
// It returns nil if the value is not valid or is a nil interface.
18+
func safeInterface(v reflect.Value) any {
19+
if !v.IsValid() {
20+
return nil
21+
}
22+
return v.Interface()
23+
}
24+
1625
// typedDecodeHook takes a raw DecodeHookFunc (an any) and turns
1726
// it into the proper DecodeHookFunc type, such as DecodeHookFuncType.
1827
func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc {
@@ -44,10 +53,16 @@ func cachedDecodeHook(raw DecodeHookFunc) func(from reflect.Value, to reflect.Va
4453
switch f := typedDecodeHook(raw).(type) {
4554
case DecodeHookFuncType:
4655
return func(from reflect.Value, to reflect.Value) (any, error) {
56+
if !from.IsValid() {
57+
return f(reflect.TypeOf((*any)(nil)).Elem(), to.Type(), nil)
58+
}
4759
return f(from.Type(), to.Type(), from.Interface())
4860
}
4961
case DecodeHookFuncKind:
5062
return func(from reflect.Value, to reflect.Value) (any, error) {
63+
if !from.IsValid() {
64+
return f(reflect.Invalid, to.Kind(), nil)
65+
}
5166
return f(from.Kind(), to.Kind(), from.Interface())
5267
}
5368
case DecodeHookFuncValue:
@@ -70,8 +85,14 @@ func DecodeHookExec(
7085
) (any, error) {
7186
switch f := typedDecodeHook(raw).(type) {
7287
case DecodeHookFuncType:
88+
if !from.IsValid() {
89+
return f(reflect.TypeOf((*any)(nil)).Elem(), to.Type(), nil)
90+
}
7391
return f(from.Type(), to.Type(), from.Interface())
7492
case DecodeHookFuncKind:
93+
if !from.IsValid() {
94+
return f(reflect.Invalid, to.Kind(), nil)
95+
}
7596
return f(from.Kind(), to.Kind(), from.Interface())
7697
case DecodeHookFuncValue:
7798
return f(from, to)
@@ -92,7 +113,7 @@ func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
92113
}
93114
return func(f reflect.Value, t reflect.Value) (any, error) {
94115
var err error
95-
data := f.Interface()
116+
data := safeInterface(f)
96117

97118
newFrom := f
98119
for _, c := range cached {
@@ -102,8 +123,11 @@ func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
102123
}
103124
if v, ok := data.(reflect.Value); ok {
104125
newFrom = v
105-
} else {
126+
} else if data != nil {
106127
newFrom = reflect.ValueOf(data)
128+
} else {
129+
// Keep newFrom as invalid (zero) Value when data is nil
130+
newFrom = reflect.Value{}
107131
}
108132
}
109133

decode_hooks_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,55 @@ func TestComposeDecodeHookFunc_ReflectValueHook(t *testing.T) {
333333
}
334334
}
335335

336+
// TestComposeDecodeHookFunc_NilValue tests that ComposeDecodeHookFunc
337+
// doesn't panic when a hook returns nil (issue #121).
338+
func TestComposeDecodeHookFunc_NilValue(t *testing.T) {
339+
hook := func(f reflect.Kind, t reflect.Kind, data any) (any, error) {
340+
return data, nil
341+
}
342+
343+
f := ComposeDecodeHookFunc(hook, hook)
344+
345+
// Test with nil input - this should not panic
346+
result, err := DecodeHookExec(f, reflect.Value{}, reflect.ValueOf(""))
347+
if err != nil {
348+
t.Fatalf("unexpected error: %s", err)
349+
}
350+
if result != nil {
351+
t.Fatalf("expected nil result, got: %#v", result)
352+
}
353+
}
354+
355+
// TestComposeDecodeHookFunc_DecodeNilRemain tests the specific scenario from issue #121
356+
// where using ComposeDecodeHookFunc with DecodeNil and ,remain tag causes a panic.
357+
func TestComposeDecodeHookFunc_DecodeNilRemain(t *testing.T) {
358+
v := make(map[string]any)
359+
v["m"] = nil
360+
361+
var result struct {
362+
V map[string]any `mapstructure:",remain"`
363+
}
364+
365+
hook := func(f reflect.Kind, t reflect.Kind, data any) (any, error) {
366+
return data, nil
367+
}
368+
369+
dec, err := NewDecoder(&DecoderConfig{
370+
DecodeHook: ComposeDecodeHookFunc(hook, hook),
371+
DecodeNil: true,
372+
Result: &result,
373+
})
374+
if err != nil {
375+
t.Fatalf("unexpected error creating decoder: %s", err)
376+
}
377+
378+
// This should not panic
379+
err = dec.Decode(&v)
380+
if err != nil {
381+
t.Fatalf("unexpected error decoding: %s", err)
382+
}
383+
}
384+
336385
func TestStringToSliceHookFunc(t *testing.T) {
337386
// Test comma separator
338387
commaSuite := decodeHookTestSuite[string, []string]{

0 commit comments

Comments
 (0)