Skip to content

Commit

Permalink
Feature: nested struct validation (#1122)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
MysteriousPotato authored Aug 6, 2023
1 parent bd1113d commit b293a5c
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 50 deletions.
4 changes: 2 additions & 2 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -1710,7 +1710,7 @@ func hasValue(fl FieldLevel) bool {
if fl.(*validate).fldIsPointer && field.Interface() != nil {
return true
}
return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()
return field.IsValid() && !field.IsZero()
}
}

Expand All @@ -1734,7 +1734,7 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
if nullable && field.Interface() != nil {
return false
}
return field.IsValid() && field.Interface() == reflect.Zero(field.Type()).Interface()
return field.IsValid() && field.IsZero()
}
}

Expand Down
21 changes: 13 additions & 8 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
typeOr
typeKeys
typeEndKeys
typeNestedStructLevel
)

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

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

func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
func (v *Validate) parseFieldTagsRecursive(tag string, field reflect.StructField, alias string, hasAlias bool) (firstCtag *cTag, current *cTag) {
var t string
noAlias := len(alias) == 0
tags := strings.Split(tag, tagSeparator)
Expand All @@ -185,9 +186,9 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
// check map for alias and process new tags, otherwise process as usual
if tagsVal, found := v.aliases[t]; found {
if i == 0 {
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, field, t, true)
} else {
next, curr := v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
next, curr := v.parseFieldTagsRecursive(tagsVal, field, t, true)
current.next, current = next, curr

}
Expand Down Expand Up @@ -235,7 +236,7 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
}
}

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

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

current.tag = vals[0]
if len(current.tag) == 0 {
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, field.Name)))
}

if wrapper, ok := v.validations[current.tag]; ok {
current.fn = wrapper.fn
current.runValidationWhenNil = wrapper.runValidatinOnNil
} else {
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, fieldName)))
panic(strings.TrimSpace(fmt.Sprintf(undefinedValidation, current.tag, field.Name)))
}

if current.typeof == typeDefault && isNestedStructOrStructPtr(field) {
current.typeof = typeNestedStructLevel
}

if len(orVals) > 1 {
Expand Down Expand Up @@ -319,7 +324,7 @@ func (v *Validate) fetchCacheTag(tag string) *cTag {
// isn't parsed again.
ctag, found = v.tagCache.Get(tag)
if !found {
ctag, _ = v.parseFieldTagsRecursive(tag, "", "", false)
ctag, _ = v.parseFieldTagsRecursive(tag, reflect.StructField{}, "", false)
v.tagCache.Set(tag, ctag)
}
}
Expand Down
18 changes: 9 additions & 9 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Example #2
This validates that the value is not the data types default zero value.
For numbers ensures value is not zero. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.
Usage: required
Expand All @@ -256,7 +256,7 @@ ensures the value is not nil.
The field under validation must be present and not empty only if all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
Usage: required_if
Expand All @@ -273,7 +273,7 @@ Examples:
The field under validation must be present and not empty unless all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
Usage: required_unless
Expand All @@ -290,7 +290,7 @@ Examples:
The field under validation must be present and not empty only if any
of the other specified fields are present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.
Usage: required_with
Expand All @@ -307,7 +307,7 @@ Examples:
The field under validation must be present and not empty only if all
of the other specified fields are present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.
Usage: required_with_all
Expand All @@ -321,7 +321,7 @@ Example:
The field under validation must be present and not empty only when any
of the other specified fields are not present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.
Usage: required_without
Expand All @@ -338,7 +338,7 @@ Examples:
The field under validation must be present and not empty only when all
of the other specified fields are not present. For strings ensures value is
not "". For slices, maps, pointers, interfaces, channels and functions
ensures the value is not nil.
ensures the value is not nil. For structs ensures value is not the zero value.
Usage: required_without_all
Expand All @@ -352,7 +352,7 @@ Example:
The field under validation must not be present or not empty only if all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
Usage: excluded_if
Expand All @@ -369,7 +369,7 @@ Examples:
The field under validation must not be present or empty unless all
the other specified fields are equal to the value following the specified
field. For strings ensures value is not "". For slices, maps, pointers,
interfaces, channels and functions ensures the value is not nil.
interfaces, channels and functions ensures the value is not nil. For structs ensures value is not the zero value.
Usage: excluded_unless
Expand Down
8 changes: 8 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,11 @@ func panicIf(err error) {
panic(err.Error())
}
}

func isNestedStructOrStructPtr(v reflect.StructField) bool {
if v.Type == nil {
return false
}
kind := v.Type.Kind()
return kind == reflect.Struct || kind == reflect.Ptr && v.Type.Elem().Kind() == reflect.Struct
}
2 changes: 1 addition & 1 deletion validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr

if ct.typeof == typeStructOnly {
goto CONTINUE
} else if ct.typeof == typeIsDefault {
} else if ct.typeof == typeIsDefault || ct.typeof == typeNestedStructLevel {
// set Field Level fields
v.slflParent = parent
v.flField = current
Expand Down
Loading

0 comments on commit b293a5c

Please sign in to comment.