Skip to content

Commit ff5d967

Browse files
authored
feat(decoder-configs): add flag to force decoding nil input in decoder config (#42)
* Add ForceDecoding Flag To Decoder Config * Add Test Cases * Fix Documentation * Remove Extra Check * Do Not Override Zero Fields Check * Use Map As Input For Tests * Rename Flag And Simplify godoc * Fix Test Name * Fix Wording of godoc * Use interface{} Instead of map[string]interface{} * Fix Test Case * Fix Variable Name * Fix godoc * Address Feedback From PR Review * Change Logic To Set InputVal And Not Erase OutputVal * Add Additional Test With Type Hook * Add Extra Test Case And Extract Bool Expression Into Variable * Fix Typo * Add More Test Cases * Rename Variable * Fix Test Case * Address Feedback From PR Review * Simplify Test Strings * Use More Descriptive Test Name * Add Test Cases For Append Hook Signed-off-by: Mahad Zaryab <[email protected]> * Run Linter Signed-off-by: Mahad Zaryab <[email protected]> * Remove Debug Statement Signed-off-by: Mahad Zaryab <[email protected]> --------- Signed-off-by: Mahad Zaryab <[email protected]>
1 parent c97971d commit ff5d967

File tree

2 files changed

+188
-7
lines changed

2 files changed

+188
-7
lines changed

mapstructure.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ type DecoderConfig struct {
278278
// field name or tag. Defaults to `strings.EqualFold`. This can be used
279279
// to implement case-sensitive tag values, support snake casing, etc.
280280
MatchName func(mapKey, fieldName string) bool
281+
282+
// DecodeNil, if set to true, will cause the DecodeHook (if present) to run
283+
// even if the input is nil. This can be used to provide default values.
284+
DecodeNil bool
281285
}
282286

283287
// A Decoder takes a raw interface value and turns it into structured
@@ -451,6 +455,8 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e
451455
}
452456
}
453457

458+
decodeNil := d.config.DecodeNil && d.config.DecodeHook != nil
459+
454460
if input == nil {
455461
// If the data is nil, then we don't set anything, unless ZeroFields is set
456462
// to true.
@@ -461,17 +467,27 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e
461467
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
462468
}
463469
}
464-
return nil
470+
471+
if !decodeNil {
472+
return nil
473+
}
465474
}
466475

467476
if !inputVal.IsValid() {
468-
// If the input value is invalid, then we just set the value
469-
// to be the zero value.
470-
outVal.Set(reflect.Zero(outVal.Type()))
471-
if d.config.Metadata != nil && name != "" {
472-
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
477+
if !decodeNil {
478+
// If the input value is invalid, then we just set the value
479+
// to be the zero value.
480+
outVal.Set(reflect.Zero(outVal.Type()))
481+
if d.config.Metadata != nil && name != "" {
482+
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
483+
}
484+
return nil
473485
}
474-
return nil
486+
487+
// If we get here, we have an untyped nil so the type of the input is assumed.
488+
// We do this because all subsequent code requires a valid value for inputVal.
489+
var mapVal map[string]interface{}
490+
inputVal = reflect.MakeMap(reflect.TypeOf(mapVal))
475491
}
476492

477493
if d.cachedDecodeHook != nil {

mapstructure_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3083,6 +3083,171 @@ func TestDecoder_IgnoreUntaggedFieldsWithStruct(t *testing.T) {
30833083
}
30843084
}
30853085

3086+
func TestDecoder_CanPerformDecodingForNilInputs(t *testing.T) {
3087+
t.Parallel()
3088+
3089+
type Transformed struct {
3090+
Message string
3091+
When string
3092+
}
3093+
3094+
helloHook := func(reflect.Type, reflect.Type, interface{}) (interface{}, error) {
3095+
return Transformed{Message: "hello"}, nil
3096+
}
3097+
goodbyeHook := func(reflect.Type, reflect.Type, interface{}) (interface{}, error) {
3098+
return Transformed{Message: "goodbye"}, nil
3099+
}
3100+
appendHook := func(from reflect.Value, to reflect.Value) (interface{}, error) {
3101+
if from.Kind() == reflect.Map {
3102+
stringMap := from.Interface().(map[string]interface{})
3103+
stringMap["when"] = "see you later"
3104+
return stringMap, nil
3105+
}
3106+
return from.Interface(), nil
3107+
}
3108+
3109+
tests := []struct {
3110+
name string
3111+
decodeNil bool
3112+
input interface{}
3113+
result Transformed
3114+
expectedResult Transformed
3115+
decodeHook DecodeHookFunc
3116+
}{
3117+
{
3118+
name: "decodeNil=true for nil input with hook",
3119+
decodeNil: true,
3120+
input: nil,
3121+
decodeHook: helloHook,
3122+
expectedResult: Transformed{Message: "hello"},
3123+
},
3124+
{
3125+
name: "decodeNil=true for nil input without hook",
3126+
decodeNil: true,
3127+
input: nil,
3128+
expectedResult: Transformed{Message: ""},
3129+
},
3130+
{
3131+
name: "decodeNil=false for nil input with hook",
3132+
decodeNil: false,
3133+
input: nil,
3134+
decodeHook: helloHook,
3135+
expectedResult: Transformed{Message: ""},
3136+
},
3137+
{
3138+
name: "decodeNil=false for nil input without hook",
3139+
decodeNil: false,
3140+
input: nil,
3141+
expectedResult: Transformed{Message: ""},
3142+
},
3143+
{
3144+
name: "decodeNil=true for non-nil input without hook",
3145+
decodeNil: true,
3146+
input: map[string]interface{}{"message": "bar"},
3147+
expectedResult: Transformed{Message: "bar"},
3148+
},
3149+
{
3150+
name: "decodeNil=true for non-nil input with hook",
3151+
decodeNil: true,
3152+
input: map[string]interface{}{"message": "bar"},
3153+
decodeHook: goodbyeHook,
3154+
expectedResult: Transformed{Message: "goodbye"},
3155+
},
3156+
{
3157+
name: "decodeNil=false for non-nil input without hook",
3158+
decodeNil: false,
3159+
input: map[string]interface{}{"message": "bar"},
3160+
expectedResult: Transformed{Message: "bar"},
3161+
},
3162+
{
3163+
name: "decodeNil=false for non-nil input with hook",
3164+
decodeNil: false,
3165+
input: map[string]interface{}{"message": "bar"},
3166+
decodeHook: goodbyeHook,
3167+
expectedResult: Transformed{Message: "goodbye"},
3168+
},
3169+
{
3170+
name: "decodeNil=true for nil input without hook and non-empty result",
3171+
decodeNil: true,
3172+
input: nil,
3173+
result: Transformed{Message: "foo"},
3174+
expectedResult: Transformed{Message: "foo"},
3175+
},
3176+
{
3177+
name: "decodeNil=true for nil input with hook and non-empty result",
3178+
decodeNil: true,
3179+
input: nil,
3180+
result: Transformed{Message: "foo"},
3181+
decodeHook: helloHook,
3182+
expectedResult: Transformed{Message: "hello"},
3183+
},
3184+
{
3185+
name: "decodeNil=false for nil input without hook and non-empty result",
3186+
decodeNil: false,
3187+
input: nil,
3188+
result: Transformed{Message: "foo"},
3189+
expectedResult: Transformed{Message: "foo"},
3190+
},
3191+
{
3192+
name: "decodeNil=false for nil input with hook and non-empty result",
3193+
decodeNil: false,
3194+
input: nil,
3195+
result: Transformed{Message: "foo"},
3196+
decodeHook: helloHook,
3197+
expectedResult: Transformed{Message: "foo"},
3198+
},
3199+
{
3200+
name: "decodeNil=false for non-nil input with hook that appends a value",
3201+
decodeNil: false,
3202+
input: map[string]interface{}{"message": "bar"},
3203+
decodeHook: appendHook,
3204+
expectedResult: Transformed{Message: "bar", When: "see you later"},
3205+
},
3206+
{
3207+
name: "decodeNil=true for non-nil input with hook that appends a value",
3208+
decodeNil: true,
3209+
input: map[string]interface{}{"message": "bar"},
3210+
decodeHook: appendHook,
3211+
expectedResult: Transformed{Message: "bar", When: "see you later"},
3212+
},
3213+
{
3214+
name: "decodeNil=true for nil input with hook that appends a value",
3215+
decodeNil: true,
3216+
decodeHook: appendHook,
3217+
expectedResult: Transformed{When: "see you later"},
3218+
},
3219+
{
3220+
name: "decodeNil=false for nil input with hook that appends a value",
3221+
decodeNil: false,
3222+
decodeHook: appendHook,
3223+
expectedResult: Transformed{},
3224+
},
3225+
}
3226+
3227+
for _, test := range tests {
3228+
t.Run(test.name, func(t *testing.T) {
3229+
config := &DecoderConfig{
3230+
Result: &test.result,
3231+
DecodeNil: test.decodeNil,
3232+
DecodeHook: test.decodeHook,
3233+
}
3234+
3235+
decoder, err := NewDecoder(config)
3236+
if err != nil {
3237+
t.Fatalf("err: %s", err)
3238+
}
3239+
3240+
if err := decoder.Decode(test.input); err != nil {
3241+
t.Fatalf("got an err: %s", err)
3242+
}
3243+
3244+
if test.result != test.expectedResult {
3245+
t.Errorf("result should be: %#v, got %#v", test.expectedResult, test.result)
3246+
}
3247+
})
3248+
}
3249+
}
3250+
30863251
func testSliceInput(t *testing.T, input map[string]interface{}, expected *Slice) {
30873252
var result Slice
30883253
err := Decode(input, &result)

0 commit comments

Comments
 (0)