Skip to content

Commit f1acffd

Browse files
joeybloggsjoeybloggs
authored andcommitted
Add Struct Level Validation!
1 parent 1570c9b commit f1acffd

File tree

5 files changed

+457
-38
lines changed

5 files changed

+457
-38
lines changed

README.md

Lines changed: 131 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -200,38 +200,143 @@ func ValidateValuer(field reflect.Value) interface{} {
200200
}
201201
```
202202

203+
Struct Level Validation
204+
```go
205+
package main
206+
207+
import (
208+
"fmt"
209+
"reflect"
210+
211+
"gopkg.in/go-playground/validator.v8"
212+
)
213+
214+
// User contains user information
215+
type User struct {
216+
FirstName string `json:"fname"`
217+
LastName string `json:"lname"`
218+
Age uint8 `validate:"gte=0,lte=130"`
219+
Email string `validate:"required,email"`
220+
FavouriteColor string `validate:"hexcolor|rgb|rgba"`
221+
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
222+
}
223+
224+
// Address houses a users address information
225+
type Address struct {
226+
Street string `validate:"required"`
227+
City string `validate:"required"`
228+
Planet string `validate:"required"`
229+
Phone string `validate:"required"`
230+
}
231+
232+
var validate *validator.Validate
233+
234+
func main() {
235+
236+
config := &validator.Config{TagName: "validate"}
237+
238+
validate = validator.New(config)
239+
validate.RegisterStructValidation(UserStructLevelValidation, User{})
240+
241+
validateStruct()
242+
}
243+
244+
// UserStructLevelValidation contains custom struct level validations that don't always
245+
// make sense at the field validation level. For Example this function validates that either
246+
// FirstName or LastName exist; could have done that with a custom field validation but then
247+
// would have had to add it to both fields duplicating the logic + overhead, this way it's
248+
// only validated once.
249+
//
250+
// NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way
251+
// hooks right into validator and you can combine with validation tags and still have a
252+
// common error output format.
253+
func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) {
254+
255+
user := structLevel.CurrentStruct.Interface().(User)
256+
257+
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
258+
structLevel.ReportError(reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname")
259+
structLevel.ReportError(reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname")
260+
}
261+
262+
// plus can to more, even with different tag than "fnameorlname"
263+
}
264+
265+
func validateStruct() {
266+
267+
address := &Address{
268+
Street: "Eavesdown Docks",
269+
Planet: "Persphone",
270+
Phone: "none",
271+
City: "Unknown",
272+
}
273+
274+
user := &User{
275+
FirstName: "",
276+
LastName: "",
277+
Age: 45,
278+
279+
FavouriteColor: "#000",
280+
Addresses: []*Address{address},
281+
}
282+
283+
// returns nil or ValidationErrors ( map[string]*FieldError )
284+
errs := validate.Struct(user)
285+
286+
if errs != nil {
287+
288+
fmt.Println(errs) // output: Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag
289+
// Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag
290+
err := errs.(validator.ValidationErrors)["User.FirstName"]
291+
fmt.Println(err.Field) // output: FirstName
292+
fmt.Println(err.Tag) // output: fnameorlname
293+
fmt.Println(err.Kind) // output: string
294+
fmt.Println(err.Type) // output: string
295+
fmt.Println(err.Param) // output:
296+
fmt.Println(err.Value) // output:
297+
298+
// from here you can create your own error messages in whatever language you wish
299+
return
300+
}
301+
302+
// save user to database
303+
}
304+
```
305+
203306
Benchmarks
204307
------
205308
###### Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 using Go 1.5.1
206309
```go
207310
$ go test -cpu=4 -bench=. -benchmem=true
208311
PASS
209-
BenchmarkFieldSuccess-4 5000000 291 ns/op 16 B/op 1 allocs/op
210-
BenchmarkFieldFailure-4 5000000 294 ns/op 16 B/op 1 allocs/op
211-
BenchmarkFieldDiveSuccess-4 500000 3498 ns/op 528 B/op 28 allocs/op
212-
BenchmarkFieldDiveFailure-4 300000 4094 ns/op 928 B/op 32 allocs/op
213-
BenchmarkFieldCustomTypeSuccess-4 3000000 460 ns/op 32 B/op 2 allocs/op
214-
BenchmarkFieldCustomTypeFailure-4 2000000 758 ns/op 400 B/op 4 allocs/op
215-
BenchmarkFieldOrTagSuccess-4 1000000 1393 ns/op 32 B/op 2 allocs/op
216-
BenchmarkFieldOrTagFailure-4 1000000 1181 ns/op 432 B/op 6 allocs/op
217-
BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1218 ns/op 80 B/op 5 allocs/op
218-
BenchmarkStructSimpleCustomTypeFailure-4 1000000 1748 ns/op 624 B/op 11 allocs/op
219-
BenchmarkStructPartialSuccess-4 1000000 1392 ns/op 400 B/op 11 allocs/op
220-
BenchmarkStructPartialFailure-4 1000000 1938 ns/op 816 B/op 16 allocs/op
221-
BenchmarkStructExceptSuccess-4 2000000 903 ns/op 368 B/op 9 allocs/op
222-
BenchmarkStructExceptFailure-4 1000000 1381 ns/op 400 B/op 11 allocs/op
223-
BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1215 ns/op 128 B/op 6 allocs/op
224-
BenchmarkStructSimpleCrossFieldFailure-4 1000000 1781 ns/op 560 B/op 11 allocs/op
225-
BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1801 ns/op 160 B/op 8 allocs/op
226-
BenchmarkStructSimpleCrossStructCrossFieldFailure-4 1000000 2357 ns/op 592 B/op 13 allocs/op
227-
BenchmarkStructSimpleSuccess-4 1000000 1161 ns/op 48 B/op 3 allocs/op
228-
BenchmarkStructSimpleFailure-4 1000000 1818 ns/op 624 B/op 11 allocs/op
229-
BenchmarkStructSimpleSuccessParallel-4 5000000 375 ns/op 48 B/op 3 allocs/op
230-
BenchmarkStructSimpleFailureParallel-4 2000000 757 ns/op 624 B/op 11 allocs/op
231-
BenchmarkStructComplexSuccess-4 200000 8053 ns/op 432 B/op 27 allocs/op
232-
BenchmarkStructComplexFailure-4 100000 12634 ns/op 3335 B/op 69 allocs/op
233-
BenchmarkStructComplexSuccessParallel-4 1000000 2718 ns/op 432 B/op 27 allocs/op
234-
BenchmarkStructComplexFailureParallel-4 300000 5086 ns/op 3336 B/op 69 allocs/op
312+
BenchmarkFieldSuccess-4 5000000 305 ns/op 16 B/op 1 allocs/op
313+
BenchmarkFieldFailure-4 5000000 301 ns/op 16 B/op 1 allocs/op
314+
BenchmarkFieldDiveSuccess-4 500000 3544 ns/op 528 B/op 28 allocs/op
315+
BenchmarkFieldDiveFailure-4 300000 4120 ns/op 928 B/op 32 allocs/op
316+
BenchmarkFieldCustomTypeSuccess-4 3000000 465 ns/op 32 B/op 2 allocs/op
317+
BenchmarkFieldCustomTypeFailure-4 2000000 769 ns/op 400 B/op 4 allocs/op
318+
BenchmarkFieldOrTagSuccess-4 1000000 1372 ns/op 32 B/op 2 allocs/op
319+
BenchmarkFieldOrTagFailure-4 1000000 1218 ns/op 432 B/op 6 allocs/op
320+
BenchmarkStructLevelValidationSuccess-4 2000000 840 ns/op 160 B/op 6 allocs/op
321+
BenchmarkStructLevelValidationFailure-4 1000000 1443 ns/op 592 B/op 11 allocs/op
322+
BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1262 ns/op 80 B/op 5 allocs/op
323+
BenchmarkStructSimpleCustomTypeFailure-4 1000000 1812 ns/op 624 B/op 11 allocs/op
324+
BenchmarkStructPartialSuccess-4 1000000 1419 ns/op 400 B/op 11 allocs/op
325+
BenchmarkStructPartialFailure-4 1000000 1967 ns/op 816 B/op 16 allocs/op
326+
BenchmarkStructExceptSuccess-4 2000000 954 ns/op 368 B/op 9 allocs/op
327+
BenchmarkStructExceptFailure-4 1000000 1422 ns/op 400 B/op 11 allocs/op
328+
BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1286 ns/op 128 B/op 6 allocs/op
329+
BenchmarkStructSimpleCrossFieldFailure-4 1000000 1885 ns/op 560 B/op 11 allocs/op
330+
BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1948 ns/op 176 B/op 9 allocs/op
331+
BenchmarkStructSimpleCrossStructCrossFieldFailure-4 500000 2491 ns/op 608 B/op 14 allocs/op
332+
BenchmarkStructSimpleSuccess-4 1000000 1239 ns/op 48 B/op 3 allocs/op
333+
BenchmarkStructSimpleFailure-4 1000000 1891 ns/op 624 B/op 11 allocs/op
334+
BenchmarkStructSimpleSuccessParallel-4 5000000 386 ns/op 48 B/op 3 allocs/op
335+
BenchmarkStructSimpleFailureParallel-4 2000000 842 ns/op 624 B/op 11 allocs/op
336+
BenchmarkStructComplexSuccess-4 200000 8604 ns/op 512 B/op 30 allocs/op
337+
BenchmarkStructComplexFailure-4 100000 13332 ns/op 3416 B/op 72 allocs/op
338+
BenchmarkStructComplexSuccessParallel-4 1000000 2929 ns/op 512 B/op 30 allocs/op
339+
BenchmarkStructComplexFailureParallel-4 300000 5220 ns/op 3416 B/op 72 allocs/op
235340
```
236341

237342
How to Contribute

benchmarks_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,32 @@ func BenchmarkFieldOrTagFailure(b *testing.B) {
6767
}
6868
}
6969

70+
func BenchmarkStructLevelValidationSuccess(b *testing.B) {
71+
72+
validate.RegisterStructValidation(StructValidationTestStructSuccess, TestStruct{})
73+
74+
tst := &TestStruct{
75+
String: "good value",
76+
}
77+
78+
for n := 0; n < b.N; n++ {
79+
validate.Struct(tst)
80+
}
81+
}
82+
83+
func BenchmarkStructLevelValidationFailure(b *testing.B) {
84+
85+
validate.RegisterStructValidation(StructValidationTestStruct, TestStruct{})
86+
87+
tst := &TestStruct{
88+
String: "good value",
89+
}
90+
91+
for n := 0; n < b.N; n++ {
92+
validate.Struct(tst)
93+
}
94+
}
95+
7096
func BenchmarkStructSimpleCustomTypeSuccess(b *testing.B) {
7197

7298
validate.RegisterCustomTypeFunc(ValidateValuerType, (*sql.Valuer)(nil), valuer{})
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"gopkg.in/go-playground/validator.v8"
8+
)
9+
10+
// User contains user information
11+
type User struct {
12+
FirstName string `json:"fname"`
13+
LastName string `json:"lname"`
14+
Age uint8 `validate:"gte=0,lte=130"`
15+
Email string `validate:"required,email"`
16+
FavouriteColor string `validate:"hexcolor|rgb|rgba"`
17+
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
18+
}
19+
20+
// Address houses a users address information
21+
type Address struct {
22+
Street string `validate:"required"`
23+
City string `validate:"required"`
24+
Planet string `validate:"required"`
25+
Phone string `validate:"required"`
26+
}
27+
28+
var validate *validator.Validate
29+
30+
func main() {
31+
32+
config := &validator.Config{TagName: "validate"}
33+
34+
validate = validator.New(config)
35+
validate.RegisterStructValidation(UserStructLevelValidation, User{})
36+
37+
validateStruct()
38+
}
39+
40+
// UserStructLevelValidation contains custom struct level validations that don't always
41+
// make sense at the field validation level. For Example this function validates that either
42+
// FirstName or LastName exist; could have done that with a custom field validation but then
43+
// would have had to add it to both fields duplicating the logic + overhead, this way it's
44+
// only validated once.
45+
//
46+
// NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way
47+
// hooks right into validator and you can combine with validation tags and still have a
48+
// common error output format.
49+
func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) {
50+
51+
user := structLevel.CurrentStruct.Interface().(User)
52+
53+
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
54+
structLevel.ReportError(reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname")
55+
structLevel.ReportError(reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname")
56+
}
57+
58+
// plus can to more, even with different tag than "fnameorlname"
59+
}
60+
61+
func validateStruct() {
62+
63+
address := &Address{
64+
Street: "Eavesdown Docks",
65+
Planet: "Persphone",
66+
Phone: "none",
67+
City: "Unknown",
68+
}
69+
70+
user := &User{
71+
FirstName: "",
72+
LastName: "",
73+
Age: 45,
74+
75+
FavouriteColor: "#000",
76+
Addresses: []*Address{address},
77+
}
78+
79+
// returns nil or ValidationErrors ( map[string]*FieldError )
80+
errs := validate.Struct(user)
81+
82+
if errs != nil {
83+
84+
fmt.Println(errs) // output: Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag
85+
// Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag
86+
err := errs.(validator.ValidationErrors)["User.FirstName"]
87+
fmt.Println(err.Field) // output: FirstName
88+
fmt.Println(err.Tag) // output: fnameorlname
89+
fmt.Println(err.Kind) // output: string
90+
fmt.Println(err.Type) // output: string
91+
fmt.Println(err.Param) // output:
92+
fmt.Println(err.Value) // output:
93+
94+
// from here you can create your own error messages in whatever language you wish
95+
return
96+
}
97+
98+
// save user to database
99+
}

0 commit comments

Comments
 (0)