Go ozzo validation

Time:2021-11-30

brief introduction

ozzo-validationIt is a very powerful and flexible data verification library. Different from other data verification libraries based on struct tag,ozzo-validationIt is considered that struct tag is easy to make mistakes in the process of use. Because struct tag is essentially a string, which is completely based on string parsing. It can not use the static checking mechanism of the language. It is easy to make mistakes unknowingly, and it is difficult to detect errors in the actual code.

ozzo-validationIt is advocated to specify rules with code for verification. actuallyozzoIt is a set of framework to assist in the development of web applications, including ORM libraryozzo-dbx, routing Libraryozzo-routing, log Libraryozzo-log, configuration libraryozzo-configAnd the most famous and widely used data verification libraryozzo-validation。 The author even came up with a template for developing web applicationsgo-rest-api

Quick use

The code in this article uses go modules.

Create directory and initialize:

$ mkdir ozzo-validation && cd ozzo-validation
$ go mod init github.com/darjun/go-daily-lib/ozzo-validation

installozzo-validationLibrary:

$ go get -u github.com/go-ozzo/ozzo-validation/v4

ozzo-validationThe programs are written intuitively:

package main

import (
  "fmt"

  "github.com/go-ozzo/ozzo-validation/v4/is"
  "github.com/go-ozzo/ozzo-validation/v4"
)

func main() {
  name := "darjun"

  err := validation.Validate(name,
    validation.Required,
    validation.Length(2, 10),
    is.URL)
  fmt.Println(err)
}

ozzo-validationUse functionValidate()The basic type value is verified. The first parameter passed in is the data to be verified, and then one or more verification rules are passed in with variable parameters. In the above example, a string is verified. We use code to express rules:

  • validation.Required: indicates that the value must be set. For a string, it cannot be empty;
  • validation.Length(2, 10): specify the range of length;
  • is.URLisA large number of auxiliary methods are built in the sub package,is.URLThe limit value must be in URL format.

Validate()The function checks the data in sequence according to the incoming rules until a rule fails or all rules succeed. If a rule returns a failure, the following rule is skipped and an error is returned directly. If the data passes all the rules, anil

Run the above program output:

must be a valid URL

Because the string “darjun” is obviously not a legal URL. If removedis.URLRule, run the outputnil

structural morphology

useValidateStruct()Function can verify a structure object. We need to specify the verification rules for each field in the structure in turn:

type User struct {
  Name  string
  Age   int
  Email string
}

func validateUser(u *User) error {
  err := validation.ValidateStruct(u,
    validation.Field(&u.Name, validation.Required, validation.Length(2, 10)),
    validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)),
    validation.Field(&u.Email, validation.Required, validation.Length(10, 50), is.Email))

  return err
}

ValidateStruct()Accept the pointer of a structure as the first parameter, and specify the rules of each field in turn. Field rule usagevalidation.Field()Function that accepts a pointer to a specific field followed by one or more rules. Above, we limit the length of names to [2,10], the age to [1,200] (let’s say that humans can live up to 200 years now), and the length of e-mail to [10,50], and useis.EmailRestriction it must be a legal email address. At the same time, these three fields are required (in Chinese)validation.RequiredRestricted).

Then we construct a legalUserObject and an illegalUserObject, respectively verify:

func main() {
  u1 := &User {
    Name: "darjun",
    Age: 18,
    Email: "[email protected]",
  }
  fmt.Println("user1:", validateUser(u1))

  u2 := &User {
    Name: "lidajun12345",
    Age: 201,
    Email: "lidajun's email",
  }
  fmt.Println("user2:", validateUser(u2))
}

Program run output:

user1: <nil>
user2: Age: must be no greater than 200; Email: must be a valid email address; Name: the length must be between 2 and 10.

For structures,validationVerify the incoming rules for each field in turn. For a field, if a rule fails to verify, the following rules will be skipped,Continue to verify the next field。 If the verification of a field fails, the result will contain error information about the field, as in the above example.

Map

Sometimes data is saved in amapInstead of a structure. You can usevalidation.Map()Specify verificationmapThe rules,validation.Map()Required in rulesvalidation.Key()Specify one or more rules corresponding to each key in turn. FinallymapType of data andvalidation.Map()Pass rules tovalidation.Validate()Function verification:

func validateUser(u map[string]interface{}) error {
  err := validation.Validate(u, validation.Map(
    validation.Key("name", validation.Required, validation.Length(2, 10)),
    validation.Key("age", validation.Required, validation.Min(1), validation.Max(200)),
    validation.Key("email", validation.Required, validation.Length(10, 50), is.Email),
  ))

  return err
}

func main() {
  u1 := map[string]interface{} {
    "name": "darjun",
    "age": 18,
    "email": "[email protected]",
  }
  fmt.Println("user1:", validateUser(u1))

  u2 := map[string]interface{} {
    "name": "lidajun12345",
    "age": 201,
    "email": "lidajun's email",
  }
  fmt.Println("user2:", validateUser(u2))
}

We modified the above example to usemap[string]interface{}storageUserInformation.mapThe verification of is similar to that of the structurevalidation.Map()The order of the keys specified in is checked in turn. If a key verification fails, an error message is recorded. Finally, the error information of all keys is summarized and returned. Running program:

user1: <nil>
user2: age: must be no greater than 200; email: must be a valid email address; name: the length must be between 2 and 10.

Verifiable type

ozzo-validationThe library provides an interfaceValidatable

type Validatable interface {
  // Validate validates the data and returns an error if validation fails.
  Validate() error
}

Everything has come trueValidatableThe types of interfaces are verifiable.validation.Validate()When a function verifies a certain type of data, it first verifies all the rules passed into the function. If these rules are passed, thenValidate()Function to determine whether the type is implementedValidatbaleInterface. If so, call itsValidate()Method. Let’s take the example aboveUserType implementationValidatableInterface:

type User struct {
  Name   string
  Age    int
  Gender string
  Email  string
}

func (u *User) Validate() error {
  err := validation.ValidateStruct(u,
    validation.Field(&u.Name, validation.Required, validation.Length(2, 10)),
    validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)),
    validation.Field(&u.Gender, validation.Required, validation.In("male", "female")),
    validation.Field(&u.Email, validation.Required, validation.Length(10, 50), is.Email))

  return err
}

becauseUserRealizedValidatableInterface, we can call it directlyValidate()Function verification:

func main() {
  u1 := &User{
    Name:   "darjun",
    Age:    18,
    Gender: "male",
    Email:  "[email protected]",
  }
  fmt.Println("user1:", validation.Validate(u1, validation.NotNil))

  u2 := &User{
    Name:  "lidajun12345",
    Age:   201,
    Email: "lidajun's email",
  }
  fmt.Println("user2:", validation.Validate(u2, validation.NotNil))
}

Passed inNotNilAfter verification,Validate()The function also callsUser.Validate()Method.

It should be noted that in the implementationValidatableType of interfaceValidate()Method cannot be called directly on a value of this typevalidation.Validate()Function, which results in infinite recursion:

type UserName string

func (n UserName) Validate() error {
  return validation.Validate(n,
    validation.Required, validation.Length(2, 10))
}

func main() {
  var n1, n2 UserName = "dj", "lidajun12345"

  fmt.Println("username1:", validation.Validate(n1))
  fmt.Println("username2:", validation.Validate(n2))
}

We are based onstringA new type is definedUserName, regulationUserNameIt is not empty and its length is in the range of [2, 10]. But aboveValidate()Method willUserNameVariable of typenFunction passed invalidation.Validate()。 An internal check of the function foundUserNameRealizedValidatableInterface, it will be called againValidate()Method, resulting in infinite recursion.

We just need to simplynTurn intostringType:

func (n UserName) Validate() error {
  return validation.Validate(string(n),
    validation.Required, validation.Length(2, 10))
}

Collection of verifiable types

Validate()Function is a verifiable type for elements (that is, it implementsValidatableWhen verifying the collection (slice / array / map, etc.) of the interface, its elements will be called in turnValidate()Method, and the final verification returns avalidation.ErrorsType. This is actually amap[string]errorType. The key is the key of the element (for slices and arrays, it is the index, formapIs the key), and the value is the wrong value. Example:

func main() {
  u1 := &User{
    Name:   "darjun",
    Age:    18,
    Gender: "male",
    Email:  "[email protected]",
  }
  u2 := &User{
    Name:  "lidajun12345",
    Age:   201,
    Email: "lidajun's email",
  }

  userSlice := []*User{u1, u2}
  userMap := map[string]*User{
    "user1": u1,
    "user2": u2,
  }

  fmt.Println("user slice:", validation.Validate(userSlice))
  fmt.Println("user map:", validation.Validate(userMap))
}

userSliceThe verification error of the second element in the slice will be in the key of the result1(index),userMapMiddle keyuser2The error of verification will be in the key of the resultuser2Return from. Operation results:

user slice: 1: (Age: must be no greater than 200; Email: must be a valid email address; Gender: cannot be blank; Name: the length must be between 2 and 10.).
user map: user2: (Age: must be no greater than 200; Email: must be a valid email address; Gender: cannot be blank; Name: the length must be between 2 and 10.).

If we need each element in the collection to meet certain rules, we can usevalidation.Each()Function. For example, ourUserThe object has multiple mailboxes, and the format of each mailbox address is required to be legal:

type User struct {
  Name   string
  Age    int
  Emails []string
}

func (u *User) Validate() error {
  return validation.ValidateStruct(u,
    validation.Field(&u.Emails, validation.Each(is.Email)))
}

func main() {
  u := &User{
    Name: "dj",
    Age:  18,
    Emails: []string{
      "[email protected]",
      "don't know",
    },
  }
  fmt.Println(validation.Validate(u))
}

The error message will indicate which location data is illegal:

Emails: (1: must be a valid email address.).

Conditional rules

We can set rules for another field according to the value of one field. For example, ourUserObject has two fields: BooleanStudentString indicating whether or not you are still a studentSchoolIndicates the school. stayStudentbytrueField whenSchoolMust exist and be in the range of [10, 20]:

type User struct {
  Name    string
  Age     int
  Student bool
  School  string
}

func (u *User) Validate() error {
  return validation.ValidateStruct(u,
    validation.Field(&u.Name, validation.Required, validation.Length(2, 10)),
    validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)),
    validation.Field(&u.School, validation.When(u.Student, validation.Required, validation.Length(10, 20))))
}

func main() {
  u1 := &User{
    Name:    "dj",
    Age:     18,
    Student: true,
  }

  u2 := &User{
    Name: "lidajun",
    Age:  31,
  }

  fmt.Println("user1:", validation.Validate(u1))
  fmt.Println("user2:", validation.Validate(u2))
}

We usevalidation.When()Function, which takes a Boolean value as the first parameter and one or more rules as the following variable parameters. Only when the first parameter istrueIf yes, the following rule verification is performed.

u1Because the field is setStudentbytrue, soSchoolField cannot be empty.u2becauseStudent=falseSchoolFields are optional. function:

user1: School: cannot be blank.
user2: <nil>

When checking the registered user’s information, we ensure that the user must set an email or mobile phone number. You can also use conditional rules:

type User struct {
  Email string
  Phone string
}

func (u *User) Validate() error {
  return validation.ValidateStruct(u,
    validation.Field(&u.Email, validation.When(u.Phone == "", validation.Required.Error("Either email or phone is required."), is.Email)),
    validation.Field(&u.Phone, validation.When(u.Email == "", validation.Required.Error("Either email or phone is required."), is.Alphanumeric)))
}

func main() {
  u1 := &User{}

  u2 := &User{
    Email: "[email protected]",
  }

  u3 := &User{
    Phone: "17301251652",
  }

  u4 := &User{
    Email: "[email protected]",
    Phone: "17301251652",
  }

  fmt.Println("user1:", validation.Validate(u1))
  fmt.Println("user2:", validation.Validate(u2))
  fmt.Println("user3:", validation.Validate(u3))
  fmt.Println("user4:", validation.Validate(u4))
}

IfPhoneField is empty,EmailMust be set. Conversely, ifEmailField is empty,PhoneMust be set. All rules can be calledError()Method to set a custom error message. Run output:

user1: Email: Either email or phone is required.; Phone: Either email or phone is required..
user2: <nil>
user3: <nil>
user4: <nil>

Custom rules

In addition to the rules provided by the library, we can also define our own rules. The rule is implemented as a function of the following types:

func Validate(value interface{}) error

Next, we implement a function to check whether the IP address is legal. Here we introduce a librarycommonregex。 This library contains most commonly used regular expressions. I also wrote an article about the use of this library before. Go is the commonregex of one library per day. If you are interested, you can go and have a look.

func checkIP(value interface{}) error {
  ip, ok := value.(string)
  if !ok {
    return errors.New("ip must be string")
  }

  ipList := commonregex.IPs(ip)
  if len(ipList) != 1 || ipList[0] != ip {
    return errors.New("invalid ip format")
  }

  return nil
}

Then define a network address structure and verification methodvalidation.By()The function uses a custom check function:

type Addr struct {
  IP   string
  Port int
}

func (a *Addr) Validate() error {
  return validation.ValidateStruct(a,
    validation.Field(&a.IP, validation.Required, validation.By(checkIP)),
    validation.Field(&a.Port, validation.Min(1024), validation.Max(65536)))
}

verification:

func main() {
  a1 := &Addr{
    IP:   "127.0.0.1",
    Port: 6666,
  }

  a2 := &Addr{
    IP:   "xxx.yyy.zzz.hhh",
    Port: 7777,
  }

  fmt.Println("addr1:", validation.Validate(a1))
  fmt.Println("addr2:", validation.Validate(a2))
}

function:

addr1: <nil>
addr2: IP: invalid ip format.

Rule group

It is inconvenient to specify rules one by one each time. At this time, we can form common verification rules into a rule group, which can be used directly when necessary. For example, the legal user name agreed in our project must be ASCII letters and numbers, with a length of 10-20. The user name must not be empty. Rule group is nothing special. It is just a slice of rules:

var NameRule = []validation.Rule{
  validation.Required,
  is.Alphanumeric,
  validation.Length(10, 20),
}

func main() {
  name1 := "lidajun12345"
  name2 := "[email protected]!#$%"
  name3 := "short"
  name4 := "looooooooooooooooooong"

  fmt.Println("name1:", validation.Validate(name1, NameRule...))
  fmt.Println("name2:", validation.Validate(name2, NameRule...))
  fmt.Println("name3:", validation.Validate(name3, NameRule...))
  fmt.Println("name4:", validation.Validate(name4, NameRule...))
}

function:

name1: <nil>
name2: must contain English letters and digits only
name3: the length must be between 10 and 20
name4: the length must be between 10 and 20

summary

ozzo-validationAdvocate code specification rules instead of error pronestruct tag, and provides a large number of built-in rules. useozzo-validationThe code written is clear, easy to read, and compiler friendly (many errors are exposed at compile time). This paper introducesozzo-validationThe core of the basic use of the library is two functionsValidate()andValidateStruct()The former is used to verify basic types or verifiable types, and the latter is used to verify structures. In the actual coding process, the structure is generally implementedValidatbaleThe interface changes it to a verifiable type and then callsValidate()Function verification.

ozzo-validationYou can also verify collections, customize verification rules, and define general verification groups. besides,ozzo-validationThere are also many advanced features, such as custom errors, based oncontext.ContextCheck, use regular expressions to define rules, etc. if you are interested, you can explore it yourself.

If you find a fun and easy-to-use go language library, you are welcome to submit an issue on GitHub, the daily library of go

reference resources

  1. ozzo-validation GitHub:github.com/go-ozzo/ozzo-validation
  2. go-rest-api GitHub:github.com/qiangxue/go-rest-api
  3. Go commonregex of one library per day: https://darjun.github.io/2020/09/05/godailylib/commonregex/
  4. Go one library a day GitHub: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to my WeChat official account, GoUpUp, learn together and make progress together.