Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,11 +347,17 @@ func isUnique(fl FieldLevel) bool {

if param == "" {
m := reflect.MakeMap(reflect.MapOf(elem, v.Type()))
var nilCount int

for i := 0; i < field.Len(); i++ {
m.SetMapIndex(reflect.Indirect(field.Index(i)), v)
e := reflect.Indirect(field.Index(i))
if !e.IsValid() {
nilCount++
continue
}
m.SetMapIndex(e, v)
}
return field.Len() == m.Len()
return field.Len()-nilCount == m.Len() && nilCount <= 1
}

sf, ok := elem.FieldByName(param)
Expand All @@ -366,14 +372,20 @@ func isUnique(fl FieldLevel) bool {

m := reflect.MakeMap(reflect.MapOf(sfTyp, v.Type()))
var fieldlen int
var nilCount int
for i := 0; i < field.Len(); i++ {
key := reflect.Indirect(reflect.Indirect(field.Index(i)).FieldByName(param))
e := reflect.Indirect(field.Index(i))
if !e.IsValid() {
nilCount++
continue
}
key := reflect.Indirect(e.FieldByName(param))
if key.IsValid() {
fieldlen++
m.SetMapIndex(key, v)
}
}
return fieldlen == m.Len()
return fieldlen == m.Len() && nilCount <= 1
case reflect.Map:
var m reflect.Value
if field.Type().Elem().Kind() == reflect.Ptr {
Expand All @@ -382,11 +394,17 @@ func isUnique(fl FieldLevel) bool {
m = reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
}

var nilCount int
for _, k := range field.MapKeys() {
m.SetMapIndex(reflect.Indirect(field.MapIndex(k)), v)
e := reflect.Indirect(field.MapIndex(k))
if !e.IsValid() {
nilCount++
continue
}
m.SetMapIndex(e, v)
}

return field.Len() == m.Len()
return field.Len()-nilCount == m.Len() && nilCount <= 1
default:
if parent := fl.Parent(); parent.Kind() == reflect.Struct {
uniqueField := parent.FieldByName(param)
Expand Down
100 changes: 100 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10931,6 +10931,106 @@ func TestUniqueValidationStructPtrSlice(t *testing.T) {
PanicMatches(t, func() { _ = validate.Var(testStructs, "unique=C") }, "Bad field name C")
}

func TestUniqueValidationNilPtrSlice(t *testing.T) {
validate := New()

type Inner struct {
Name string
}

t.Run("unique_field_single_nil_ptr", func(t *testing.T) {
// A single nil element should not panic and should pass (it's unique by itself)
s := struct {
F1 []*Inner `validate:"unique=Name"`
}{F1: []*Inner{nil}}
errs := validate.Struct(s)
if errs != nil {
t.Fatalf("single nil element should pass unique=Name validation, got: %v", errs)
}
})

t.Run("unique_field_duplicate_nil_ptrs", func(t *testing.T) {
// Two nil elements should not panic and should fail (not unique)
s := struct {
F1 []*Inner `validate:"unique=Name"`
}{F1: []*Inner{nil, nil}}
errs := validate.Struct(s)
if errs == nil {
t.Fatal("duplicate nil elements should fail unique=Name validation")
}
})

t.Run("unique_field_nil_and_non_nil", func(t *testing.T) {
// One nil and one non-nil should pass (they are different)
s := struct {
F1 []*Inner `validate:"unique=Name"`
}{F1: []*Inner{nil, {Name: "abc"}}}
errs := validate.Struct(s)
if errs != nil {
t.Fatalf("nil and non-nil elements should pass unique=Name validation, got: %v", errs)
}
})

t.Run("unique_no_param_single_nil_ptr", func(t *testing.T) {
// A single nil element without param should not panic
s := struct {
F1 []*Inner `validate:"unique"`
}{F1: []*Inner{nil}}
errs := validate.Struct(s)
if errs != nil {
t.Fatalf("single nil element should pass unique validation, got: %v", errs)
}
})

t.Run("unique_no_param_duplicate_nil_ptrs", func(t *testing.T) {
// Two nil elements without param should fail
s := struct {
F1 []*Inner `validate:"unique"`
}{F1: []*Inner{nil, nil}}
errs := validate.Struct(s)
if errs == nil {
t.Fatal("duplicate nil elements should fail unique validation")
}
})

t.Run("unique_no_param_nil_and_non_nil", func(t *testing.T) {
// One nil and one non-nil should pass
s := struct {
F1 []*Inner `validate:"unique"`
}{F1: []*Inner{nil, {Name: "abc"}}}
errs := validate.Struct(s)
if errs != nil {
t.Fatalf("nil and non-nil elements should pass unique validation, got: %v", errs)
}
})

t.Run("unique_map_nil_values", func(t *testing.T) {
// Map with nil pointer values should not panic
m := map[string]*string{"one": nil, "two": nil}
errs := validate.Var(m, "unique")
if errs == nil {
t.Fatal("duplicate nil map values should fail unique validation")
}
})

t.Run("unique_map_single_nil_value", func(t *testing.T) {
m := map[string]*string{"one": nil}
errs := validate.Var(m, "unique")
if errs != nil {
t.Fatalf("single nil map value should pass unique validation, got: %v", errs)
}
})

t.Run("unique_map_nil_and_non_nil", func(t *testing.T) {
a := "hello"
m := map[string]*string{"one": nil, "two": &a}
errs := validate.Var(m, "unique")
if errs != nil {
t.Fatalf("nil and non-nil map values should pass unique validation, got: %v", errs)
}
})
}

func TestHTMLValidation(t *testing.T) {
tests := []struct {
param string
Expand Down