package main import ( "encoding/json" "fmt" "reflect" "strings" "github.com/go-playground/validator/v10" ) type validationError struct { Namespace string `json:"namespace"` // can differ when a custom TagNameFunc is registered or Field string `json:"field"` // by passing alt name to ReportError like below StructNamespace string `json:"structNamespace"` StructField string `json:"structField"` Tag string `json:"tag"` ActualTag string `json:"actualTag"` Kind string `json:"kind"` Type string `json:"type"` Value string `json:"value"` Param string `json:"param"` Message string `json:"message"` } type Gender uint const ( Male Gender = iota + 1 Female Intersex ) func (gender Gender) String() string { terms := []string{"Male", "Female", "Intersex"} if gender < Male || gender > Intersex { return "unknown" } return terms[gender] } // User contains user information type User struct { FirstName string `json:"fname"` LastName string `json:"lname"` Age uint8 `validate:"gte=0,lte=130"` Email string `json:"e-mail" validate:"required,email"` FavouriteColor string `validate:"hexcolor|rgb|rgba"` Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage... Gender Gender `json:"gender" validate:"required,gender_custom_validation"` } // Address houses a users address information type Address struct { Street string `validate:"required"` City string `validate:"required"` Planet string `validate:"required"` Phone string `validate:"required"` } // use a single instance of Validate, it caches struct info var validate *validator.Validate func main() { validate = validator.New() // register function to get tag name from json tags. validate.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] if name == "-" { return "" } return name }) // register validation for 'User' // NOTE: only have to register a non-pointer type for 'User', validator // internally dereferences during it's type checks. validate.RegisterStructValidation(UserStructLevelValidation, User{}) // register a custom validation for user genre on a line // validates that an enum is within the interval err := validate.RegisterValidation("gender_custom_validation", func(fl validator.FieldLevel) bool { value := fl.Field().Interface().(Gender) return value.String() != "unknown" }) if err != nil { fmt.Println(err) return } // build 'User' info, normally posted data etc... address := &Address{ Street: "Eavesdown Docks", Planet: "Persphone", Phone: "none", City: "Unknown", } user := &User{ FirstName: "", LastName: "", Age: 45, Email: "Badger.Smith@gmail", FavouriteColor: "#000", Addresses: []*Address{address}, } // returns InvalidValidationError for bad validation input, nil or ValidationErrors ( []FieldError ) err = validate.Struct(user) if err != nil { // this check is only needed when your code could produce // an invalid value for validation such as interface with nil // value most including myself do not usually have code like this. if _, ok := err.(*validator.InvalidValidationError); ok { fmt.Println(err) return } for _, err := range err.(validator.ValidationErrors) { e := validationError{ Namespace: err.Namespace(), Field: err.Field(), StructNamespace: err.StructNamespace(), StructField: err.StructField(), Tag: err.Tag(), ActualTag: err.ActualTag(), Kind: fmt.Sprintf("%v", err.Kind()), Type: fmt.Sprintf("%v", err.Type()), Value: fmt.Sprintf("%v", err.Value()), Param: err.Param(), Message: err.Error(), } indent, err := json.MarshalIndent(e, "", " ") if err != nil { fmt.Println(err) panic(err) } fmt.Println(string(indent)) } // from here you can create your own error messages in whatever language you wish return } // save user to database } // UserStructLevelValidation contains custom struct level validations that don't always // make sense at the field validation level. For Example this function validates that either // FirstName or LastName exist; could have done that with a custom field validation but then // would have had to add it to both fields duplicating the logic + overhead, this way it's // only validated once. // // NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way // hooks right into validator and you can combine with validation tags and still have a // common error output format. func UserStructLevelValidation(sl validator.StructLevel) { user := sl.Current().Interface().(User) if len(user.FirstName) == 0 && len(user.LastName) == 0 { sl.ReportError(user.FirstName, "fname", "FirstName", "fnameorlname", "") sl.ReportError(user.LastName, "lname", "LastName", "fnameorlname", "") } // plus can do more, even with different tag than "fnameorlname" }