From 78fc1dba0b72bf178379577c8ec9877f62c86c2a Mon Sep 17 00:00:00 2001 From: Junaid Islam Date: Tue, 17 Jun 2025 18:58:28 +0530 Subject: [PATCH 1/4] Added test to check that []byte does not panie when used with required_if --- validator_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/validator_test.go b/validator_test.go index 00a410101..8e69f5317 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14753,3 +14753,19 @@ func TestOmitZeroWithMaps(t *testing.T) { Equal(t, err2, nil) // No error }) } +func TestTextOrBytesNoPanic(t *testing.T) { + // Tests that the TextOrBytes struct does not panic when both fields are nil + type TextOrBytes struct { + Text string `validate:"required_if=Bytes nil"` + Bytes []byte `validate:"required_if=Text nil"` + } + + defer func() { + if r := recover(); r != nil { + t.Errorf("Unexpected panic occurred: %v", r) + } + }() + + validate := New(WithRequiredStructEnabled()) + validate.Struct(&TextOrBytes{}) +} From 96fd3cb510ea3baa3432909fcdd80a169722b756 Mon Sep 17 00:00:00 2001 From: Junaid Islam Date: Wed, 18 Jun 2025 11:30:01 +0530 Subject: [PATCH 2/4] Fix: Added validation to check if slice or map is nil and created different case to check length of array since arrays cannot be nil in go --- baked_in.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/baked_in.go b/baked_in.go index f1387320e..0a2eb423a 100644 --- a/baked_in.go +++ b/baked_in.go @@ -1853,7 +1853,13 @@ func requireCheckFieldValue( case reflect.Float64: return field.Float() == asFloat64(value) - case reflect.Slice, reflect.Map, reflect.Array: + case reflect.Slice, reflect.Map: + if value == "nil" { + return field.IsNil() + } + return int64(field.Len()) == asInt(value) + case reflect.Array: + // Arrays can't be nil, so only compare lengths return int64(field.Len()) == asInt(value) case reflect.Bool: From 579709f1db3b52e6cc8a0614bf57bc71aeb8efd1 Mon Sep 17 00:00:00 2001 From: Junaid Islam Date: Wed, 18 Jun 2025 11:32:18 +0530 Subject: [PATCH 3/4] Added testcases for validation of nil with slices and maps and length validation for arrays when used with required if --- validator_test.go | 220 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/validator_test.go b/validator_test.go index 8e69f5317..7e84d5e26 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14769,3 +14769,223 @@ func TestTextOrBytesNoPanic(t *testing.T) { validate := New(WithRequiredStructEnabled()) validate.Struct(&TextOrBytes{}) } +func TestRequiredIfWithNilBytesSlice(t *testing.T) { + // Tests the behavior of required_if with nil byte slices + type TextOrBytes struct { + Text string `validate:"required_if=Bytes nil"` + Bytes []byte `validate:"required_if=Text nil"` + } + + validate := New(WithRequiredStructEnabled()) + + t.Run("Text empty, Bytes empty but not nil", func(t *testing.T) { + // Test 1: Text empty, Bytes empty but not nil + obj := TextOrBytes{ + Text: "", + Bytes: []byte{}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Bytes is not nil, just empty + }) + + t.Run("Text empty, Bytes nil", func(t *testing.T) { + // Test 2: Text empty, Bytes nil + obj := TextOrBytes{ + Text: "", + Bytes: nil, + } + err := validate.Struct(obj) + + NotEqual(t, err, nil) // Error - Text is required when Bytes is nil + }) + + t.Run("Text populated, Bytes nil", func(t *testing.T) { + // Test 3: Text populated, Bytes nil + obj := TextOrBytes{ + Text: "Hello", + Bytes: nil, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Text has value when Bytes is nil + }) + + t.Run("Text populated, Bytes empty but not nil", func(t *testing.T) { + // Test 4: Text populated, Bytes empty but not nil + obj := TextOrBytes{ + Text: "Hello", + Bytes: []byte{}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Neither field is nil + }) + + t.Run("Text empty, Bytes populated", func(t *testing.T) { + // Test 5: Text empty, Bytes populated + obj := TextOrBytes{ + Text: "", + Bytes: []byte("World"), + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Bytes has value when Text is empty + }) + + t.Run("Both fields populated", func(t *testing.T) { + // Test 6: Both fields populated + obj := TextOrBytes{ + Text: "Hello", + Bytes: []byte("World"), + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Both fields have values + }) +} +func TestRequiredIfWithMaps(t *testing.T) { + // Tests the behavior of required_if with maps + type TextOrMap struct { + Text string `validate:"required_if=Data nil"` + Data map[string]interface{} `validate:"required_if=Text nil"` + } + + validate := New(WithRequiredStructEnabled()) + + t.Run("Text empty, Map nil", func(t *testing.T) { + // Test 1: Text empty, Map nil + obj := TextOrMap{ + Text: "", + Data: nil, + } + err := validate.Struct(obj) + + NotEqual(t, err, nil) // Error - Text is required when Data is nil + }) + + t.Run("Text populated, Map nil", func(t *testing.T) { + // Test 2: Text populated, Map nil + obj := TextOrMap{ + Text: "Hello", + Data: nil, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Text has value when Data is nil + }) + + t.Run("Text empty, Map empty but not nil", func(t *testing.T) { + // Test 3: Text empty, Map empty but not nil + obj := TextOrMap{ + Text: "", + Data: map[string]interface{}{}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Data is not nil, just empty + }) + + t.Run("Text populated, Map empty but not nil", func(t *testing.T) { + // Test 4: Text populated, Map empty but not nil + obj := TextOrMap{ + Text: "Hello", + Data: map[string]interface{}{}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Neither field is nil + }) + + t.Run("Text empty, Map populated", func(t *testing.T) { + // Test 5: Text empty, Map populated + obj := TextOrMap{ + Text: "", + Data: map[string]interface{}{"key": "value"}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Data has value when Text is empty + }) + + t.Run("Both fields populated", func(t *testing.T) { + // Test 6: Both fields populated + obj := TextOrMap{ + Text: "Hello", + Data: map[string]interface{}{"key": "value"}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Both fields have values + }) +} +func TestRequiredIfWithArrays(t *testing.T) { + // Tests the behavior of required_if with arrays + type TextOrArray struct { + Text string `validate:"required_if=Data 3"` // Check if array length is 3 + Data [3]string `validate:"required_if=Text nil"` // Check if Text is empty + } + + // Also test with a different length to verify behavior + type TextOrSmallArray struct { + Text string `validate:"required_if=Data 2"` // Check if array length is 2 (which it never will be for [3]string) + Data [3]string `validate:"required_if=Text nil"` // Check if Text is empty + } + + validate := New(WithRequiredStructEnabled()) + + t.Run("Array length always matches declared size", func(t *testing.T) { + // Test 1: Text empty, Array has length 3 + obj := TextOrArray{ + Text: "", + Data: [3]string{}, // Zero values but length is 3 + } + err := validate.Struct(obj) + + NotEqual(t, err, nil) // Error - Text is required when Data length equals 3 + }) + + t.Run("Text populated, Array always has declared length", func(t *testing.T) { + // Test 2: Text populated, Array has length 3 + obj := TextOrArray{ + Text: "Hello", + Data: [3]string{}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Text has value + }) + + t.Run("Length check that never matches doesn't trigger validation", func(t *testing.T) { + // Test 3: Text empty, Array has length 3 but we check for length 2 + obj := TextOrSmallArray{ + Text: "", + Data: [3]string{}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Data length never equals 2 + }) + + t.Run("Array values don't affect length check", func(t *testing.T) { + // Test 4: Text empty, Array partially populated but still length 3 + obj := TextOrArray{ + Text: "", + Data: [3]string{"One", "Two", ""}, + } + err := validate.Struct(obj) + + NotEqual(t, err, nil) // Error - Text is required when Data length equals 3 + }) + + t.Run("Array values with Text populated passes validation", func(t *testing.T) { + // Test 5: Text populated, Array with values + obj := TextOrArray{ + Text: "Hello", + Data: [3]string{"One", "Two", "Three"}, + } + err := validate.Struct(obj) + + Equal(t, err, nil) // No error - Text has value + }) +} From f20d93d9e0c3e65ce6ec548381113d4be63e4d6f Mon Sep 17 00:00:00 2001 From: Junaid Islam Date: Wed, 18 Jun 2025 11:56:43 +0530 Subject: [PATCH 4/4] Removed initial test which was added to reproduce the issue. Comprehensive tests for all cases remain --- validator_test.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/validator_test.go b/validator_test.go index 7e84d5e26..1273b474c 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14753,22 +14753,6 @@ func TestOmitZeroWithMaps(t *testing.T) { Equal(t, err2, nil) // No error }) } -func TestTextOrBytesNoPanic(t *testing.T) { - // Tests that the TextOrBytes struct does not panic when both fields are nil - type TextOrBytes struct { - Text string `validate:"required_if=Bytes nil"` - Bytes []byte `validate:"required_if=Text nil"` - } - - defer func() { - if r := recover(); r != nil { - t.Errorf("Unexpected panic occurred: %v", r) - } - }() - - validate := New(WithRequiredStructEnabled()) - validate.Struct(&TextOrBytes{}) -} func TestRequiredIfWithNilBytesSlice(t *testing.T) { // Tests the behavior of required_if with nil byte slices type TextOrBytes struct {