Skip to content
Merged
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
121 changes: 85 additions & 36 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,61 +336,110 @@ func isOneOfCI(fl FieldLevel) bool {
func isUnique(fl FieldLevel) bool {
field := fl.Field()
param := fl.Param()
v := reflect.ValueOf(struct{}{})

// sentinel used as map key for nil values
var nilKey = struct{}{}

switch field.Kind() {
case reflect.Slice, reflect.Array:
elem := field.Type().Elem()
if elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
seen := make(map[interface{}]struct{})

if param == "" {
m := reflect.MakeMap(reflect.MapOf(elem, v.Type()))
for i := 0; i < field.Len(); i++ {
elem := field.Index(i)

// -------- unique (no param) --------
if param == "" {
var key interface{}

if elem.Kind() == reflect.Ptr {
if elem.IsNil() {
key = nilKey
} else {
key = elem.Elem().Interface() // <-- compare underlying value
}
} else {
key = elem.Interface()
}

for i := 0; i < field.Len(); i++ {
m.SetMapIndex(reflect.Indirect(field.Index(i)), v)
if _, ok := seen[key]; ok {
return false
}
seen[key] = struct{}{}
continue
}
return field.Len() == m.Len()
}

sf, ok := elem.FieldByName(param)
if !ok {
panic(fmt.Sprintf("Bad field name %s", param))
}
// -------- unique=Field --------

sfTyp := sf.Type
if sfTyp.Kind() == reflect.Ptr {
sfTyp = sfTyp.Elem()
}
if elem.Kind() == reflect.Ptr {
if elem.IsNil() {
if _, ok := seen[nilKey]; ok {
return false
}
seen[nilKey] = struct{}{}
continue
}
elem = elem.Elem()
}

m := reflect.MakeMap(reflect.MapOf(sfTyp, v.Type()))
var fieldlen int
for i := 0; i < field.Len(); i++ {
key := reflect.Indirect(reflect.Indirect(field.Index(i)).FieldByName(param))
if key.IsValid() {
fieldlen++
m.SetMapIndex(key, v)
if elem.Kind() != reflect.Struct {
panic(fmt.Sprintf("Bad field type %s", elem.Type()))
}

sf := elem.FieldByName(param)
if !sf.IsValid() {
panic(fmt.Sprintf("Bad field name %s", param))
}

var key interface{}

if sf.Kind() == reflect.Ptr {
if sf.IsNil() {
key = nilKey
} else {
key = sf.Elem().Interface()
}
} else {
key = sf.Interface()
}

if _, ok := seen[key]; ok {
return false
}
seen[key] = struct{}{}
}
return fieldlen == m.Len()

return true

case reflect.Map:
var m reflect.Value
if field.Type().Elem().Kind() == reflect.Ptr {
m = reflect.MakeMap(reflect.MapOf(field.Type().Elem().Elem(), v.Type()))
} else {
m = reflect.MakeMap(reflect.MapOf(field.Type().Elem(), v.Type()))
}
seen := make(map[interface{}]struct{})

for _, k := range field.MapKeys() {
m.SetMapIndex(reflect.Indirect(field.MapIndex(k)), v)
val := field.MapIndex(k)

var key interface{}

if val.Kind() == reflect.Ptr {
if val.IsNil() {
key = nilKey
} else {
key = val.Elem().Interface() // <-- compare underlying value
}
} else {
key = val.Interface()
}

if _, ok := seen[key]; ok {
return false
}
seen[key] = struct{}{}
}

return field.Len() == m.Len()
return true

default:
if parent := fl.Parent(); parent.Kind() == reflect.Struct {
uniqueField := parent.FieldByName(param)
if uniqueField == reflect.ValueOf(nil) {
if !uniqueField.IsValid() {
panic(fmt.Sprintf("Bad field name provided %s", param))
}

Expand Down
108 changes: 108 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10931,6 +10931,114 @@ 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)
}
})

t.Run("unique_slice_with_nil_and_zero_value_struct", func(t *testing.T) {
s := []*Inner{nil, {Name: ""}}
errs := validate.Var(s, "unique")
if errs != nil {
t.Fatalf("nil and zero value struct should pass unique validation, got: %v", errs)
}
})
}

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