diff --git a/mapstructure.go b/mapstructure.go index 256ee63f..52aa2c3e 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -77,6 +77,7 @@ type DecoderConfig struct { // - single values are converted to slices if required. Each // element is weakly decoded. For example: "4" can become []int{4} // if the target type is an int slice. + // - nils are accepted as zero values of any type // WeaklyTypedInput bool @@ -236,6 +237,11 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e } if input == nil { + // We convert nils into zero values only if WeaklyTypedInput is set to true + if !d.config.WeaklyTypedInput && outVal.Kind() != reflect.Ptr && outVal.Kind() != reflect.Interface { + return fmt.Errorf("'%s' should not be null (expected type: %s)", name, outVal.Kind()) + } + // If the data is nil, then we don't set anything, unless ZeroFields is set // to true. if d.config.ZeroFields { diff --git a/mapstructure_bugs_test.go b/mapstructure_bugs_test.go index ba6590a6..896adeeb 100644 --- a/mapstructure_bugs_test.go +++ b/mapstructure_bugs_test.go @@ -110,9 +110,10 @@ func TestDecode_NilValue(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { config := &DecoderConfig{ - Metadata: new(Metadata), - Result: tc.target, - ZeroFields: true, + Metadata: new(Metadata), + Result: tc.target, + ZeroFields: true, + WeaklyTypedInput: true, } decoder, err := NewDecoder(config) diff --git a/mapstructure_test.go b/mapstructure_test.go index 27ac10eb..7e51f0f2 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -239,6 +239,113 @@ func TestBasicTypes(t *testing.T) { } } +func TestNullPointers(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": nil, + "vint": nil, + "Vuint": nil, + "vbool": nil, + "Vfloat": nil, + "vsilent": nil, + "vdata": nil, + "vjsonInt": nil, + "vjsonFloat": nil, + "vjsonNumber": nil, + } + + var result Basic + err := Decode(input, &result) + + if err == nil { + t.Errorf("should be an error") + t.FailNow() + } + if !strings.Contains(err.Error(), "'Vstring' should not be null (expected type: string)") { + t.Errorf("no error for 'Vstring' in %#v", err) + } + if !strings.Contains(err.Error(), "'Vint' should not be null (expected type: int)") { + t.Errorf("no error for 'Vint' in %#v", err) + } + if !strings.Contains(err.Error(), "'Vbool' should not be null (expected type: bool)") { + t.Errorf("no error for 'Vbool' in %#v", err) + } + if !strings.Contains(err.Error(), "'Vfloat' should not be null (expected type: float64)") { + t.Errorf("no error for 'Vfloat' in %#v", err) + } + if strings.Contains(err.Error(), "Vdata") { + t.Errorf("got error for 'Vdata' in %#v", err) + } + if !strings.Contains(err.Error(), "'VjsonInt' should not be null (expected type: int)") { + t.Errorf("no error for 'VjsonInt' in %#v", err) + } + if !strings.Contains(err.Error(), "'VjsonFloat' should not be null (expected type: float64)") { + t.Errorf("no error for 'VjsonFloat' in %#v", err) + } + if !strings.Contains(err.Error(), "'VjsonNumber' should not be null (expected type: string)") { + t.Errorf("no error for 'VjsonNumber' in %#v", err) + } +} + +func TestNullPointers_WeakDecode(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": nil, + "vint": nil, + "Vuint": nil, + "vbool": nil, + "Vfloat": nil, + "vsilent": nil, + "vdata": nil, + "vjsonInt": nil, + "vjsonFloat": nil, + "vjsonNumber": nil, + } + + var result Basic + err := WeakDecode(input, &result) + + if err != nil { + t.Fatalf("got an err: %s", err) + } + + expected := Basic{} + if result != expected { + t.Errorf("result is not a zero structure: %#v", result) + } +} + +func TestNullPointers_DecodeIntoPointers(t *testing.T) { + t.Parallel() + + input := map[string]interface{}{ + "vstring": nil, + "vint": nil, + "Vuint": nil, + "vbool": nil, + "Vfloat": nil, + "vsilent": nil, + "vdata": nil, + "vjsonInt": nil, + "vjsonFloat": nil, + "vjsonNumber": nil, + } + + var result BasicPointer + err := Decode(input, &result) + + if err != nil { + t.Fatalf("got an err: %s", err) + } + + expected := BasicPointer{} + if result != expected { + t.Errorf("result is not a zero structure: %#v", result) + } +} + func TestBasic_IntWithFloat(t *testing.T) { t.Parallel() @@ -609,6 +716,24 @@ func TestDecode_Nil(t *testing.T) { } err := Decode(input, &result) + if err == nil { + t.Fatalf("should be an error") + } + + if !strings.Contains(err.Error(), "'' should not be null (expected type: struct)") { + t.Fatalf("unexpected error message: %s", err) + } +} + +func TestDecode_Nil_Weak(t *testing.T) { + t.Parallel() + + var input interface{} + result := Basic{ + Vstring: "foo", + } + + err := WeakDecode(input, &result) if err != nil { t.Fatalf("err: %s", err) } @@ -1469,21 +1594,29 @@ func TestDecodeTable(t *testing.T) { { "basic pointer to non-pointer", &BasicPointer{ - Vstring: stringPtr("vstring"), - Vint: intPtr(2), - Vuint: uintPtr(3), - Vbool: boolPtr(true), - Vfloat: floatPtr(4.56), - Vdata: interfacePtr([]byte("data")), + Vstring: stringPtr("vstring"), + Vint: intPtr(2), + Vuint: uintPtr(3), + Vbool: boolPtr(true), + Vfloat: floatPtr(4.56), + Vdata: interfacePtr([]byte("data")), + Vextra: stringPtr("extra"), + VjsonFloat: floatPtr(1.1), + VjsonInt: intPtr(5), + VjsonNumber: func() *json.Number { n := json.Number(123); return &n }(), }, &Basic{}, &Basic{ - Vstring: "vstring", - Vint: 2, - Vuint: 3, - Vbool: true, - Vfloat: 4.56, - Vdata: []byte("data"), + Vstring: "vstring", + Vint: 2, + Vuint: 3, + Vbool: true, + Vfloat: 4.56, + Vdata: []byte("data"), + Vextra: "extra", + VjsonFloat: 1.1, + VjsonInt: 5, + VjsonNumber: json.Number(123), }, false, },