Introduction to the

Ozzo-validation is a very powerful, flexible data validation library. Unlike other struct tag-based validation libraries, Ozzo-Validation considers struct tags to be error-prone. Because struct tag is essentially a string, it is completely based on the parsing of string, and it cannot make use of the static checking mechanism of the language, so it is easy to write errors unknowingly and not easily detected, and it is also difficult to detect errors in the actual code.

Ozzo-validation advocates using code to specify rules for validation. Ozzo is actually a set of frameworks for developing Web applications, including the ORM library Ozzo-DBX, the routing library Ozzo-Routing, the log library ozzo-log, the configuration library ozzo-Config and, most famously, Ozzo-validation is the most widely used data validation library. The author even came up with a go-REST-API template for developing Web applications.

Quick to 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
Copy the code

Install the Ozzo-Validation library:

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

Ozzo-validation is straightforward to write:

package main

import (
  "fmt"

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

func main(a) {
  name := "darjun"

  err := validation.Validate(name,
    validation.Required,
    validation.Length(2.10),
    is.URL)
  fmt.Println(err)
}
Copy the code

Ozzo-validation validates primitive type values using the Validate() function, passing in the data to be validated as the first parameter, followed by one or more validation rules as mutable parameters. The above example validates a string. We express rules in code:

  • validation.Required: indicates that the value must be set, which in the case of strings cannot be null;
  • validation.Length(2, 10): Specifies the range of length;
  • is.URL:isThere are a number of helper methods built into the sub-packages,is.URLThe limiting value must be in URL format.

The Validate() function validates the data in sequence against the incoming rules until a rule fails or all rules succeed. If a rule returns a failure, it skips subsequent rules and returns an error. If the data passes all the rules, a nil is returned.

Run the above program output:

must be a valid URL
Copy the code

Because the string “darjun” is clearly not a valid URL. If the is.url rule is removed, run output nil.

The structure of the body

The ValidateStruct() function ValidateStruct validates a structure object. We need to specify the validation 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
}
Copy the code

ValidateStruct() takes a pointer to a structure as the first argument, which in turn specifies the rules for each field. Field rules are specified using the validation.field () function, which accepts a pointer to a specific Field followed by one or more rules. Above we restrict names to be between [2, 10], ages to be between [1, 200] (let’s assume humans live at most 200 years these days), Email addresses to be between [10, 50], and using IS.email it must be a legitimate Email address. All three fields are also Required (restricted with validation.Required).

Then we construct a valid User object and an invalid User object and verify them respectively:

func main(a) {
  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))
}
Copy the code

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.
Copy the code

For a structure, Validation validates the incoming rule for each field in turn. For a field, if the verification of one rule fails, the next rule is skipped and the verification of the next field continues. If a field fails to be verified, error information about the field is displayed in the result, as shown in the preceding example.

Map

Sometimes the data is stored in a map rather than a structure. You can use validation.map () to specify rules for validation of a Map. Validation.map () specifies one or more rules for each Key in turn using validation.key (). Validation.validate () validates the map data and validation.map () rules:

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(a) {
  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))
}
Copy the code

We modified the above example to store User information using map[string]interface{}. Maps are validated in the same way as structures, in the order of the keys specified in Validation.map (). If a key fails to be verified, an error message is recorded. A final summary of all key error messages is returned. Run the 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.
Copy the code

Verifiable type

The ozzo-Validation library provides an interface Validatable:

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

Any type that implements the Validatable interface is a Validatable type. Validation.validate () validates all rules passed into the validation.validate () function when it validates a type of data. If these rules pass, the Validate() function determines whether the type implements a Validatbale interface. If implemented, its Validate() method is called to verify. Let’s implement the Validatable interface for the User type in the above example:

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

func (u *User) Validate(a) 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
}
Copy the code

Since User implements the Validatable interface, we can call Validate() directly:

func main(a) {
  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))
}
Copy the code

After passing the NotNil check, the Validate() function also calls the user.validate () method.

Note that validation.validate () cannot be called directly from within the Validate() method of the type that implements the Validatable interface, which results in infinite recursion:

type UserName string

func (n UserName) Validate(a) error {
  return validation.Validate(n,
    validation.Required, validation.Length(2.10))}func main(a) {
  var n1, n2 UserName = "dj"."lidajun12345"

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

We define a new type UserName based on string, specifying that UserName is non-null and in the range [2, 10]. However, the Validate() method above passes the variable n of type UserName to validation.validate (). This function internally checks that UserName implements the Validatable interface, and calls its Validate() method, causing infinite recursion.

We simply convert n to string:

func (n UserName) Validate(a) error {
  return validation.Validate(string(n),
    validation.Required, validation.Length(2.10))}Copy the code

A collection of verifiable types

When the Validate() function validates collections (slices, arrays, maps, etc.) whose elements are Validatable (that is, implements the Validatable interface), it calls the Validate() method of each element in turn, and finally returns validation.errors. This is actually a map[string]error type. The key is the element’s key (index for slice and array, key for map), and the value is an error value. Ex. :

func main(a) {
  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))
}
Copy the code

The validation error for the second element in the userSlice slice is returned in the result key 1 (index), and the validation error for the userMap key user2 is returned in the result key user2. Running 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.).
Copy the code

If you want every element in a collection to meet certain rules, you can use validation.each (). For example, our User object has multiple mailboxes and requires each mailbox address to be in a valid format:

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

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

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

The error message indicates where the data is invalid:

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

Condition rule

We can set rules for one field based on the value of another field. For example, our User object has two fields: the Boolean Student to indicate whether it is still a Student, and the string School to indicate a School. When Student is true, the field School must exist and be in the range of length [10, 20] :

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

func (u *User) Validate(a) 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(a) {
  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))
}
Copy the code

We use validation.when (), which takes a Boolean value as the first argument and one or more rules as the variable arguments that follow. Subsequent rule verification is performed only if the first argument is true.

U1 School cannot be null because Student is set to true. U2 because Student=false, School is optional. Run:

user1: School: cannot be blank.
user2: <nil>
Copy the code

When checking the registered user information, we make sure that the user must set an email or mobile phone number and can also use the conditional rule:

type User struct {
  Email string
  Phone string
}

func (u *User) Validate(a) 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(a) {
  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))
}
Copy the code

If the Phone field is empty, Email must be set. Otherwise, if the Email field is empty, Phone must be set. All rules can call the Error() method to set custom Error messages. Run output:

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

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 type:

func Validate(value interface{}) error
Copy the code

Let’s implement a function that checks whether the IP address is valid. Here we introduce a library called CommonRegex. This library contains most commonly used regular expressions. I have also written an article about the use of this library, Go Daily Library commonRegex, if you are interested, check it out.

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
}
Copy the code

Then define a network address structure and validation methods, using validation.by () with a custom validation function:

type Addr struct {
  IP   string
  Port int
}

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

Validation:

func main(a) {
  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))
}
Copy the code

Run:

addr1: <nil>
addr2: IP: invalid ip format.
Copy the code

The rule group

It is inconvenient to specify the rules one by one each time, so we can combine the commonly used verification rules into a rule group and use this group as needed. For example, the convention in our project is that a valid user name must be ASCII letters plus numbers, 10-20 in length, and the user name must not be empty. A rule group is nothing special, it’s just a slice of a rule:

var NameRule = []validation.Rule{
  validation.Required,
  is.Alphanumeric,
  validation.Length(10.20),}func main(a) {
  name1 := "lidajun12345"
  name2 := "lidajun@! # $%"
  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...))
}
Copy the code

Run:

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
Copy the code

conclusion

Ozzo-validation advocates replacing error-prone struct tags with code-specified rules and provides a number of built-in rules. Code written with Ozzo-Validation is clear, easy to read, and compiler friendly (many errors are exposed at compile time). In this article, we introduce the basic use of ozzo-Validation library. The core functions are Validate() and ValidateStruct(), the former is used to Validate basic or verifiable types, and the latter is used to Validate structures. In actual coding, a structure is usually made to implement a Validatbale interface that makes it a validable type and then validates it by calling Validate().

Ozzo-validation also validates collections, allows you to customize validation rules, and allows you to define generic validation groups. In addition, ozzo-Validation has many advanced features, such as custom errors, context-based validation, and rules defined using regular expressions.

If you find a fun and useful Go library, please Go to GitHub and submit issue😄

reference

  1. Ozzo – validation GitHub:github.com/go-ozzo/ozz…
  2. Go to rest – API GitHub:github.com/qiangxue/go…
  3. Go daily commonregex of library: darjun. Making. IO / 2020/09/05 /…
  4. GitHub: github.com/darjun/go-d…

I

My blog is darjun.github. IO

Welcome to follow my wechat public account [GoUpUp], learn together, progress together ~