fix: prevent panic on pointer fields in comparison validators - #1596
Open
1solomonwakhungu wants to merge 1 commit into
Open
Conversation
…yground#907) Comparison validators (isGte, isGt, isLte, isLt, hasLengthOf) panicked with "Bad field type *T" when called on nil pointer fields. This happened when a pointer field used required_if alongside a threshold tag (e.g. required_if=Type special,gte=2) and required_if evaluated to false, causing the threshold validator to run on the nil pointer. The fix dereferences pointer fields early in each comparison validator: nil pointers return false (no threshold is satisfied by absence of value), non-nil pointers are dereferenced and compared against the threshold using the existing type-specific logic. Additionally, traverseField now skips validators that are not marked runValidationWhenNil when encountering a nil pointer/interface, preventing unnecessary work and ensuring consistent behavior across all validators. Fixes go-playground#907
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Comparison validators (
isGte,isGt,isLte,isLt,hasLengthOf) panic with"Bad field type *T"when called on nil pointer fields. This occurs when a pointer field (e.g.*int64) usesrequired_ifalongside a threshold tag likegte=2, and therequired_ifcondition evaluates to false — causing the threshold validator to run on the nil pointer instead of being skipped.Reproduction
Root Cause
When
required_ifevaluates to false for a pointer field, the validator chain continues to the next tag (gte). The comparison validators inbaked_in.godo not handlereflect.Pointer, so they fall through to thepanic(fmt.Sprintf("Bad field type %s", field.Type()))at the end of each function.Fix
Two complementary changes:
Dereference pointer fields early in comparison validators (
isGte,isGt,isLte,isLt,hasLengthOf): At the top of each function, iffield.Kind() == reflect.Pointer, nil pointers returnfalse(no threshold is satisfied by the absence of a value), and non-nil pointers are dereferenced viafield.Elem()so the existing type-specific comparison logic handles the underlying value naturally.Skip non-nil-checkable validators on nil pointers in
traverseField: WhentraverseFieldencounters a nilreflect.Ptrorreflect.Interfaceand the current tag is not markedrunValidationWhenNil, it now returns early instead of continuing to execute validators that will panic.Design Decisions
false: A nil pointer represents the absence of a value. No threshold (gte,gt,lte,lt,len) is satisfied by absence, so returningfalseis semantically correct. This is consistent with howomitemptybehaves — if the field is nil, validation is skipped (no error), and when the field is present, the threshold applies normally.switch) is cleaner than adding areflect.Pointercase inside the switch, because it lets the existing type cases handle the dereferenced value without duplication.hasMinOfandhasMaxOfdelegate toisGteandisLterespectively, so they are covered automatically.Changes
baked_in.go: Added pointer dereference guard toisGte,isGt,isLte,isLt,hasLengthOfvalidator.go: Added nil-pointer skip intraverseFieldfor tags withoutrunValidationWhenNilvalidator_test.go: AddedTestRequiredIfWithPointerAndGtewith 4 test casesTest Plan
go build ./...passesgo test ./...passes (all existing tests + new test)go vet ./...cleanTestRequiredIfWithPointerAndGteverifies:*int64withrequired_iffalse: no panic, no error*int64withrequired_iftrue: validation error onrequired_if*int64=5withrequired_iftrue +gte=2: passes*int64=1withrequired_iftrue +gte=2: fails ongteFixes #907