Skip to content

Commit b293a5c

Browse files
Feature: nested struct validation (#1122)
## Fixes #367, #906 **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. A test has been added for custom tags, however I was not brave enough to actually update the tests for all required/excluded tag variants before getting an initial feedback, but I'm willing to do so if this ever gets any further. Same goes for documentation. The implementation supports both struct and struct pointer validations for custom tags and all required/excluded tag variants. Struct validity is evaluated first and fields are evaluated only if the struct is valid, though I'm not sure if this is the desired behavior. @go-playground/validator-maintainers
1 parent bd1113d commit b293a5c

File tree

6 files changed

+340
-50
lines changed

6 files changed

+340
-50
lines changed

baked_in.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,7 +1710,7 @@ func hasValue(fl FieldLevel) bool {
17101710
if fl.(*validate).fldIsPointer && field.Interface() != nil {
17111711
return true
17121712
}
1713-
return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()
1713+
return field.IsValid() && !field.IsZero()
17141714
}
17151715
}
17161716

@@ -1734,7 +1734,7 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
17341734
if nullable && field.Interface() != nil {
17351735
return false
17361736
}
1737-
return field.IsValid() && field.Interface() == reflect.Zero(field.Type()).Interface()
1737+
return field.IsValid() && field.IsZero()
17381738
}
17391739
}
17401740

cache.go

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

2526
const (
@@ -152,7 +153,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
152153
// and so only struct level caching can be used instead of combined with Field tag caching
153154

154155
if len(tag) > 0 {
155-
ctag, _ = v.parseFieldTagsRecursive(tag, fld.Name, "", false)
156+
ctag, _ = v.parseFieldTagsRecursive(tag, fld, "", false)
156157
} else {
157158
// even if field doesn't have validations need cTag for traversing to potential inner/nested
158159
// elements of the field.
@@ -171,7 +172,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr
171172
return cs
172173
}
173174

174-
func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
175+
func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
175176
var t string
176177
noAlias := len(alias) == 0
177178
tags := strings.Split(tag, tagSeparator)
@@ -185,9 +186,9 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
185186
// check map for alias and process new tags, otherwise process as usual
186187
if tagsVal, found := v.aliases[t]; found {
187188
if i == 0 {
188-
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
189+
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, field, t, true)
189190
} else {
190-
next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
191+
next, curr := v.parseFieldTagsRecursive(tagsVal, field, t, true)
191192
current.next, current = next, curr
192193

193194
}
@@ -235,7 +236,7 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
235236
}
236237
}
237238

238-
current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false)
239+
current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), field, "", false)
239240
continue
240241

241242
case endKeysTag:
@@ -284,14 +285,18 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
284285

285286
current.tag = vals[0]
286287
if len(current.tag) == 0 {
287-
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
288+
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, field.Name)))
288289
}
289290

290291
if wrapper, ok := v.validations[current.tag]; ok {
291292
current.fn = wrapper.fn
292293
current.runValidationWhenNil = wrapper.runValidatinOnNil
293294
} else {
294-
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
295+
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, field.Name)))
296+
}
297+
298+
if current.typeof == typeDefault && isNestedStructOrStructPtr(field) {
299+
current.typeof = typeNestedStructLevel
295300
}
296301

297302
if len(orVals) > 1 {
@@ -319,7 +324,7 @@ func (v *Validate) fetchCacheTag(tag string) *cTag {
319324
// isn't parsed again.
320325
ctag, found = v.tagCache.Get(tag)
321326
if !found {
322-
ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
327+
ctag, _ = v.parseFieldTagsRecursive(tag, reflect.StructField{}, "", false)
323328
v.tagCache.Set(tag, ctag)
324329
}
325330
}

doc.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ Example #2
247247
This validates that the value is not the data types default zero value.
248248
For numbers ensures value is not zero. For strings ensures value is
249249
not "". For slices, maps, pointers, interfaces, channels and functions
250-
ensures the value is not nil.
250+
ensures the value is not nil. For structs ensures value is not the zero value.
251251
252252
Usage: required
253253
@@ -256,7 +256,7 @@ ensures the value is not nil.
256256
The field under validation must be present and not empty only if all
257257
the other specified fields are equal to the value following the specified
258258
field. For strings ensures value is not "". For slices, maps, pointers,
259-
interfaces, channels and functions ensures the value is not nil.
259+
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
260260
261261
Usage: required_if
262262
@@ -273,7 +273,7 @@ Examples:
273273
The field under validation must be present and not empty unless all
274274
the other specified fields are equal to the value following the specified
275275
field. For strings ensures value is not "". For slices, maps, pointers,
276-
interfaces, channels and functions ensures the value is not nil.
276+
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
277277
278278
Usage: required_unless
279279
@@ -290,7 +290,7 @@ Examples:
290290
The field under validation must be present and not empty only if any
291291
of the other specified fields are present. For strings ensures value is
292292
not "". For slices, maps, pointers, interfaces, channels and functions
293-
ensures the value is not nil.
293+
ensures the value is not nil. For structs ensures value is not the zero value.
294294
295295
Usage: required_with
296296
@@ -307,7 +307,7 @@ Examples:
307307
The field under validation must be present and not empty only if all
308308
of the other specified fields are present. For strings ensures value is
309309
not "". For slices, maps, pointers, interfaces, channels and functions
310-
ensures the value is not nil.
310+
ensures the value is not nil. For structs ensures value is not the zero value.
311311
312312
Usage: required_with_all
313313
@@ -321,7 +321,7 @@ Example:
321321
The field under validation must be present and not empty only when any
322322
of the other specified fields are not present. For strings ensures value is
323323
not "". For slices, maps, pointers, interfaces, channels and functions
324-
ensures the value is not nil.
324+
ensures the value is not nil. For structs ensures value is not the zero value.
325325
326326
Usage: required_without
327327
@@ -338,7 +338,7 @@ Examples:
338338
The field under validation must be present and not empty only when all
339339
of the other specified fields are not present. For strings ensures value is
340340
not "". For slices, maps, pointers, interfaces, channels and functions
341-
ensures the value is not nil.
341+
ensures the value is not nil. For structs ensures value is not the zero value.
342342
343343
Usage: required_without_all
344344
@@ -352,7 +352,7 @@ Example:
352352
The field under validation must not be present or not empty only if all
353353
the other specified fields are equal to the value following the specified
354354
field. For strings ensures value is not "". For slices, maps, pointers,
355-
interfaces, channels and functions ensures the value is not nil.
355+
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
356356
357357
Usage: excluded_if
358358
@@ -369,7 +369,7 @@ Examples:
369369
The field under validation must not be present or empty unless all
370370
the other specified fields are equal to the value following the specified
371371
field. For strings ensures value is not "". For slices, maps, pointers,
372-
interfaces, channels and functions ensures the value is not nil.
372+
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
373373
374374
Usage: excluded_unless
375375

util.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,11 @@ func panicIf(err error) {
286286
panic(err.Error())
287287
}
288288
}
289+
290+
func isNestedStructOrStructPtr(v reflect.StructField) bool {
291+
if v.Type == nil {
292+
return false
293+
}
294+
kind := v.Type.Kind()
295+
return kind == reflect.Struct || kind == reflect.Ptr && v.Type.Elem().Kind() == reflect.Struct
296+
}

validator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr
170170

171171
if ct.typeof == typeStructOnly {
172172
goto CONTINUE
173-
} else if ct.typeof == typeIsDefault {
173+
} else if ct.typeof == typeIsDefault || ct.typeof == typeNestedStructLevel {
174174
// set Field Level fields
175175
v.slflParent = parent
176176
v.flField = current

0 commit comments

Comments
 (0)