Skip to content

Commit aa96909

Browse files
authored
Add omitnil modifier (#1187)
## Fixes Or Enhances Related issue: #1186 **Make sure that you've checked the boxes below before you submit PR:** - [x] Tests exist or have been written that cover this particular change. @go-playground/validator-maintainers
1 parent b0c7337 commit aa96909

File tree

6 files changed

+85
-3
lines changed

6 files changed

+85
-3
lines changed

baked_in.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var (
5151
endKeysTag: {},
5252
structOnlyTag: {},
5353
omitempty: {},
54+
omitnil: {},
5455
skipValidationTag: {},
5556
utf8HexComma: {},
5657
utf8Pipe: {},

cache.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
typeOr
2121
typeKeys
2222
typeEndKeys
23+
typeOmitNil
2324
)
2425

2526
const (
@@ -252,6 +253,10 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
252253
current.typeof = typeOmitEmpty
253254
continue
254255

256+
case omitnil:
257+
current.typeof = typeOmitNil
258+
continue
259+
255260
case structOnlyTag:
256261
current.typeof = typeStructOnly
257262
continue

doc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ such as min or max won't run, but if a value is set validation will run.
194194
195195
Usage: omitempty
196196
197+
# Omit Nil
198+
199+
Allows to skip the validation if the value is nil (same as omitempty, but
200+
only for the nil-values).
201+
202+
Usage: omitnil
203+
197204
# Dive
198205
199206
This tells the validator to dive into a slice, array or map and validate that

validator.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
112112
return
113113
}
114114

115+
if ct.typeof == typeOmitNil && (kind != reflect.Invalid && current.IsNil()) {
116+
return
117+
}
118+
115119
if ct.hasTag {
116120
if kind == reflect.Invalid {
117121
v.str1 = string(append(ns, cf.altName...))
@@ -233,6 +237,26 @@ OUTER:
233237
ct = ct.next
234238
continue
235239

240+
case typeOmitNil:
241+
v.slflParent = parent
242+
v.flField = current
243+
v.cf = cf
244+
v.ct = ct
245+
246+
switch field := v.Field(); field.Kind() {
247+
case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
248+
if field.IsNil() {
249+
return
250+
}
251+
default:
252+
if v.fldIsPointer && field.Interface() == nil {
253+
return
254+
}
255+
}
256+
257+
ct = ct.next
258+
continue
259+
236260
case typeEndKeys:
237261
return
238262

validator_instance.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
structOnlyTag = "structonly"
2323
noStructLevelTag = "nostructlevel"
2424
omitempty = "omitempty"
25+
omitnil = "omitnil"
2526
isdefault = "isdefault"
2627
requiredWithoutAllTag = "required_without_all"
2728
requiredWithoutTag = "required_without"

validator_test.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13196,14 +13196,14 @@ func TestSpiceDBValueFormatValidation(t *testing.T) {
1319613196
tag string
1319713197
expected bool
1319813198
}{
13199-
//Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
13199+
// Must be an asterisk OR a string containing alphanumeric characters and a restricted set a special symbols: _ | / - = +
1320013200
{"*", "spicedb=id", true},
1320113201
{`azAZ09_|/-=+`, "spicedb=id", true},
1320213202
{`a*`, "spicedb=id", false},
1320313203
{`/`, "spicedb=id", true},
1320413204
{"*", "spicedb", true},
1320513205

13206-
//Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
13206+
// Must begin and end with a lowercase letter, may also contain numbers and underscores between, min length 3, max length 64
1320713207
{"a", "spicedb=permission", false},
1320813208
{"1", "spicedb=permission", false},
1320913209
{"a1", "spicedb=permission", false},
@@ -13213,7 +13213,7 @@ func TestSpiceDBValueFormatValidation(t *testing.T) {
1321313213
{"abcdefghijklmnopqrstuvwxyz_0123456789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", true},
1321413214
{"abcdefghijklmnopqrstuvwxyz_01234_56789_abcdefghijklmnopqrstuvwxyz", "spicedb=permission", false},
1321513215

13216-
//Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
13216+
// Object types follow the same rules as permissions for the type name plus an optional prefix up to 63 characters with a /
1321713217
{"a", "spicedb=type", false},
1321813218
{"1", "spicedb=type", false},
1321913219
{"a1", "spicedb=type", false},
@@ -13606,3 +13606,47 @@ func TestTimeRequired(t *testing.T) {
1360613606
NotEqual(t, err, nil)
1360713607
AssertError(t, err.(ValidationErrors), "TestTime.Time", "TestTime.Time", "Time", "Time", "required")
1360813608
}
13609+
13610+
func TestOmitNilAndRequired(t *testing.T) {
13611+
type (
13612+
OmitEmpty struct {
13613+
Str string `validate:"omitempty,required,min=10"`
13614+
StrPtr *string `validate:"omitempty,required,min=10"`
13615+
Inner *OmitEmpty
13616+
}
13617+
OmitNil struct {
13618+
Str string `validate:"omitnil,required,min=10"`
13619+
StrPtr *string `validate:"omitnil,required,min=10"`
13620+
Inner *OmitNil
13621+
}
13622+
)
13623+
13624+
var (
13625+
validate = New(WithRequiredStructEnabled())
13626+
valid = "this is the long string to pass the validation rule"
13627+
)
13628+
13629+
t.Run("compare using valid data", func(t *testing.T) {
13630+
err1 := validate.Struct(OmitEmpty{Str: valid, StrPtr: &valid, Inner: &OmitEmpty{Str: valid, StrPtr: &valid}})
13631+
err2 := validate.Struct(OmitNil{Str: valid, StrPtr: &valid, Inner: &OmitNil{Str: valid, StrPtr: &valid}})
13632+
13633+
Equal(t, err1, nil)
13634+
Equal(t, err2, nil)
13635+
})
13636+
13637+
t.Run("compare fully empty omitempty and omitnil", func(t *testing.T) {
13638+
err1 := validate.Struct(OmitEmpty{})
13639+
err2 := validate.Struct(OmitNil{})
13640+
13641+
Equal(t, err1, nil)
13642+
AssertError(t, err2, "OmitNil.Str", "OmitNil.Str", "Str", "Str", "required")
13643+
})
13644+
13645+
t.Run("validate in deep", func(t *testing.T) {
13646+
err1 := validate.Struct(OmitEmpty{Str: valid, Inner: &OmitEmpty{}})
13647+
err2 := validate.Struct(OmitNil{Str: valid, Inner: &OmitNil{}})
13648+
13649+
Equal(t, err1, nil)
13650+
AssertError(t, err2, "OmitNil.Inner.Str", "OmitNil.Inner.Str", "Str", "Str", "required")
13651+
})
13652+
}

0 commit comments

Comments
 (0)