Fix ValidatePrivateFields: UpperFirst corrupts unexported embedded struct field paths#331
Conversation
…field lookup Co-authored-by: inhere <5302062+inhere@users.noreply.github.com> Agent-Logs-Url: https://github.com/gookit/validate/sessions/9313a2dc-1c4e-476b-b62f-c5806326f354
There was a problem hiding this comment.
Pull request overview
Fixes struct field-path normalization so ValidatePrivateFields = true can correctly access validation rules on unexported embedded struct fields (e.g. foo.Field1) without corrupting the path casing.
Changes:
- Adjust
StructData.TryGet/StructData.Setto applyUpperFirstonly as a fallback when the field path isn’t already registered. - Extend
TestValidatePrivateFieldsWhenTrueto assert the “valid value passes” case that previously failed.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
data_source.go |
Makes path normalization conditional to preserve original casing for registered (including private embedded) field paths. |
data_source_test.go |
Adds a regression assertion ensuring validation passes when the embedded field value is valid. |
| // Only uppercase if the field is not already registered with its original casing. | ||
| // This mirrors the same fix in TryGet for private embedded struct fields. | ||
| if _, ok := d.fieldNames[field]; !ok { | ||
| field = strutil.UpperFirst(field) | ||
| } | ||
| if !d.HasField(field) { // field not found |
There was a problem hiding this comment.
Set can now skip UpperFirst for registered embedded-field paths (e.g. foo.Field1), but the rest of Set does not handle fieldAtAnonymous (the switch case is empty). If d.fieldNames[field] is fieldAtAnonymous, fv remains an invalid reflect.Value and removeValuePtr(fv) will panic. Consider handling fieldAtAnonymous the same as fieldAtSubStruct (walk the path and select the nested field), or otherwise return a controlled error instead of leaving fv invalid.
| v.Validate() | ||
|
|
||
| assert.Equal(t, v.hasError, true) | ||
|
|
||
| // Field1 = 5 satisfies all rules — validation must pass | ||
| barz.foo.Field1 = 5 | ||
| v = Struct(barz) | ||
| v.Validate() | ||
| assert.Equal(t, v.hasError, false) |
There was a problem hiding this comment.
These assertions pass actual then expected to assert.Equal, which is inconsistent with the surrounding tests that use assert.Equal(t, expected, actual) (e.g. data_source_test.go:260-261, issues_test.go:511). Swapping the argument order (or using assert.True/False) will make assertion failures much easier to interpret.
ValidatePrivateFields = truehad no effect becausestrutil.UpperFirst()was unconditionally applied inTryGetandSet, turning paths likefoo.Field1intoFoo.Field1. Since the embedded struct is registered asfoo(lowercase),FieldByName("Foo")always failed silently — making validation behave as if the field didn't exist regardless of its value.The existing test masked this: it asserted
hasError == truewith an invalid value, which passed — but only because the field was invisible to the validator, not because the rule fired. Setting a valid value still produced errors.Changes
data_source.go—TryGetandSet: applyUpperFirstonly when the field is not already present ind.fieldNames. Fields registered duringparseRulesFromTag(including private embedded struct fields) retain their original casing; exported field name normalization (e.g."name"→"Name") is preserved as a fallback.data_source_test.go— ExtendTestValidatePrivateFieldsWhenTrueto assert that validation passes when the embedded field holds a valid value — the case that was previously broken:Original prompt
⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.