Skip to content

Commit 28a2ae6

Browse files
committed
add ignore fields without tag in struct arrays
1 parent 892f312 commit 28a2ae6

4 files changed

Lines changed: 66 additions & 7 deletions

File tree

README.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ if err != nil {
6868
```
6969

7070
In these examples:
71+
7172
- `vld:"min3"`: Ensures the string field Name has a minimum length of 3 characters.
7273
- `vld:"rex^[^@]+@[^@]+\\.[^@]+$"`: Validates Email against a regular expression pattern.
7374
- `vld:"min18"`: Checks that the integer field Age has a minimum value of 18.
@@ -89,7 +90,7 @@ If you want to use the `User` struct for multiple handlers like `CreateUser`, `U
8990

9091
```go
9192
type User struct {
92-
ID int `delete:"min1"`
93+
ID int `delete:"min1" get:"min1"`
9394
Name string `create:"min3" update:"min3, gr1min1"`
9495
Email string `create:"rex^[^@]+@[^@]+\\.[^@]+$" update:"rex^[^@]+@[^@]+\\.[^@]+$, gr1min1"`
9596
Age int `create:"min18" update:"min18, gr1min1"`
@@ -99,7 +100,7 @@ func CreateUser(w http.ResponseWriter, r *http.Request) {
99100
user := &User{}
100101
err := v.UnmapOrUnmarshalValidateAndUpdate(r, user, "create")
101102
if err != nil {
102-
fmt.Println("Validation failed for new user:", err)
103+
fmt.Println("Validation failed for creating user:", err)
103104
}
104105
...
105106
}
@@ -108,7 +109,7 @@ func UpdateUser(w http.ResponseWriter, r *http.Request) {
108109
user := &User{}
109110
err := v.UnmapOrUnmarshalValidateAndUpdate(r, user, "update")
110111
if err != nil {
111-
fmt.Println("Validation failed for new user:", err)
112+
fmt.Println("Validation failed for updating user:", err)
112113
}
113114
...
114115
}
@@ -117,7 +118,16 @@ func DeleteUser(w http.ResponseWriter, r *http.Request) {
117118
user := &User{}
118119
err := v.UnmapOrUnmarshalValidateAndUpdate(r, user, "delete")
119120
if err != nil {
120-
fmt.Println("Validation failed for new user:", err)
121+
fmt.Println("Validation failed for deleting user:", err)
122+
}
123+
...
124+
}
125+
126+
func GetUser(w http.ResponseWriter, r *http.Request) {
127+
user := &User{}
128+
err := v.UnmapOrUnmarshalValidateAndUpdate(r, user, "ger")
129+
if err != nil {
130+
fmt.Println("Validation failed for getting user:", err)
121131
}
122132
...
123133
}
@@ -148,6 +158,7 @@ You can do for example `vld:"max0 || ((min10 && max30) || equTest)"` for a strin
148158
### Condition types
149159

150160
Conditions have different usages per variable type:
161+
151162
- `-` - Not validating/update without validating.
152163
- `equ` - `int/float/string == condition`, `len(array) == condition`
153164
- `neq` - `int/float/string != condition`, `len(array) != condition`
@@ -179,7 +190,7 @@ type Error struct {
179190
StatusCode int `json:"status_code" vld:"min100" upd:"min100, gr1min1"`
180191
Message string `json:"message" vld:"min1" upd:"min1, gr1min1"`
181192
UnderlyingException string `json:"underlying_exception" vld:"min1, gr1min1" upd:"min1, gr1min1"`
182-
CreatedAt time.Time `json:"created_at" vld:"-"`
193+
CreatedAt time.Time `json:"created_at"`
183194
}
184195
```
185196

@@ -189,6 +200,30 @@ One of `Message` and `UnderlyingException` is required on creation.
189200

190201
---
191202

203+
# 🔒 Security
204+
205+
The validator provides built-in security through tag-based field filtering. **Only fields with the specified validation tag will be updated**, protecting sensitive fields from unauthorized modification. This tag filtering also works recursively for nested structs and arrays of structs.
206+
207+
## Tag Behavior
208+
209+
When using `ValidateAndUpdate` or similar functions with a custom tag (e.g., `"upd"`), fields are handled as follows:
210+
211+
```go
212+
type User struct {
213+
ID int `json:"id"` // No tag: NEVER updated
214+
Password string `json:"password"` // No tag: NEVER updated
215+
Name string `json:"name" upd:"-"` // Ignore tag: Updated WITHOUT validation
216+
Email string `json:"email" upd:"rex^[^@]+@[^@]+\\.[^@]+$"` // Full tag: Validated AND updated
217+
Role string `json:"role"` // No tag: NEVER updated
218+
}
219+
220+
// In your update handler:
221+
user := &User{}
222+
err := v.ValidateAndUpdate(jsonInput, user, "upd")
223+
```
224+
225+
---
226+
192227
# Testing
193228

194229
To run the tests run `go test ./...`.

validator.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,20 @@ func (r *Validator) ValidateWithValidation(jsonInput map[string]any, validations
164164
return map[string]any{}, fmt.Errorf("field %v must be of type array, was %T", validation.Key, jsonValue)
165165
}
166166

167+
validatedArray := []any{}
167168
for _, jsonValueInner := range jsonArray {
168169
jsonValueInnerMap, err := helper.GetValidMap(jsonValueInner)
169170
if err != nil {
170171
return map[string]any{}, fmt.Errorf("field %v invalid: %v", validation.Key, err.Error())
171172
}
172173

173-
_, err = r.ValidateWithValidation(jsonValueInnerMap, validation.InnerValidation)
174+
validatedInnerMap, err := r.ValidateWithValidation(jsonValueInnerMap, validation.InnerValidation)
174175
if err != nil {
175176
return map[string]any{}, fmt.Errorf("field %v invalid: %v", validation.Key, err.Error())
176177
}
178+
validatedArray = append(validatedArray, validatedInnerMap)
177179
}
180+
jsonValue = validatedArray
178181
} else if helper.IsArray(jsonValue) {
179182
err = r.ValidateValueWithParser(jsonValue, &validation)
180183
} else if helper.IsString(jsonValue) {

validatorExtract.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ func GetValidationFromStructField(tagType string, fieldValue reflect.Value, fiel
4545
validation := &model.Validation{}
4646
validation.Key = fieldType.Name
4747
if len(fieldType.Tag.Get("json")) > 0 {
48-
validation.Key = fieldType.Tag.Get("json")
48+
jsonKey := fieldType.Tag.Get("json")
49+
// Split on comma to handle omitempty and other options
50+
jsonKey = strings.Split(jsonKey, ",")[0]
51+
if jsonKey != "-" {
52+
validation.Key = jsonKey
53+
}
4954
}
5055
validation.Type = model.ReflectKindToValidatorType(fieldValue.Type().Kind())
5156
validation.Requirement = "-"

validator_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,22 @@ func TestValidateAndUpdate(t *testing.T) {
178178
require.Error(t, err, "Expected an error for invalid validation")
179179
assert.Contains(t, err.Error(), "invalid group name: gp1", "Expected error to contain 'invalid group name: gp1'")
180180
})
181+
182+
t.Run("Tag filtering in arrays of structs with custom tag", func(t *testing.T) {
183+
testStruct := &struct {
184+
Items []struct {
185+
ID string `json:"id"`
186+
Name string `json:"name" upd:"-"`
187+
Age int `json:"age" upd:"min18"`
188+
} `json:"items" upd:"-"`
189+
}{}
190+
err := r.ValidateAndUpdate(map[string]any{"items": []any{map[string]any{"id": "123", "name": "test", "age": 19}}}, testStruct, "upd")
191+
assert.NoError(t, err, "Expected no error")
192+
assert.Len(t, testStruct.Items, 1, "Items should be updated")
193+
assert.Equal(t, "", testStruct.Items[0].ID, "ID should not be updated (no upd tag)")
194+
assert.Equal(t, "test", testStruct.Items[0].Name, "Name should not be updated (has ignore upd tag)")
195+
assert.Equal(t, 19, testStruct.Items[0].Age, "Age should be updated (has full upd tag)")
196+
})
181197
}
182198

183199
func TestValidateAndUpdateWithValidation(t *testing.T) {

0 commit comments

Comments
 (0)