Custom struct field tags in Golang

Structs in Golang represent one of the most common variable types and used practically everywhere, from dealing with configuration options to marshaling of JSON or XML documents using encoding/json or encoding/xml packages. Field tags are part of the struct's field definition and allow nice and easy way to store meta data about fields for many use cases (field mapping, data validation, ORM, etc).

Basics

What's interesting about structs in general? One of the most useful features of the struct is ability to specify field name mapping. It comes very handy if you deal with external services and do a lot of data transformations. Lets take a look at the following example:

type User struct {
  Id        int       `json:"id"`
  Name      string    `json:"name"`
  Bio       string    `json:"about,omitempty"`
  Active    bool      `json:"active"`
  Admin     bool      `json:"-"`
  CreatedAt time.Time `json:"created_at"`
}

In our User struct, tags are just strings following the field's type definition and enclosed in backticks. In our example we redefine field names for JSON encoding/decoding. What that means is when JSON encoder goes over the struct fields, it will use user-defined field names instead of default capitalized names. Here's the JSON output of the struct without custom tags, produced by json.Marshal call:

{
  "Id": 1,
  "Name": "John Doe",
  "Bio": "Some Text",
  "Active": true,
  "Admin": false,
  "CreatedAt": "2016-07-16T15:32:17.957714799Z"
}

As you can see, all fields in the example output correspond to their definitions on the User struct. Now, lets add custom JSON tags and see what happens:

{
  "id": 1,
  "name": "John Doe",
  "about": "Some Text",
  "active": true,
  "created_at": "2016-07-16T15:32:17.957714799Z"
}

With custom tags we were able to reshape the output. Using json:"-" definition we tell encoder to skip the field altogether. Check out JSON and XML packages for more details and available tag options.

Roll Your Own

Now that we understand how struct tags are defined and used, lets try to make our own tag processor. To do so we need to inspect the struct and read the tag attributes. That's where the reflect package comes into play.

Pretend we're going to implement simple validation library that uses field tags to define some validation rules based on field's type. We always want to validate data before it gets saved to a database.

package main

import (
  "fmt"
  "reflect"
)

// Name of the struct tag used in examples
const tagName = "validate"

type User struct {
  Id    int    `validate:"-"`
  Name  string `validate:"presence,min=2,max=32"`
  Email string `validate:"email,required"`
}

func main() {
  user := User{
    Id:    1,
    Name:  "John Doe",
    Email: "john@example",
  }

  // TypeOf returns the reflection Type that represents the dynamic type of variable.
  // If variable is a nil interface value, TypeOf returns nil.
  t := reflect.TypeOf(user)

  // Get the type and kind of our user variable
  fmt.Println("Type:", t.Name())
  fmt.Println("Kind:", t.Kind())

  // Iterate over all available fields and read the tag value
  for i := 0; i < t.NumField(); i++ {
    // Get the field, returns https://golang.org/pkg/reflect/#StructField
    field := t.Field(i)

    // Get the field tag value
    tag := field.Tag.Get(tagName)

    fmt.Printf("%d. %v (%v), tag: '%v'\n", i+1, field.Name, field.Type.Name(), tag)
  }
}

Save and run the code. You'll see something like this:

Type: User
Kind: struct
1. Id (int), tag: '-'
2. Name (string), tag: 'presence,min=2,max=32'
3. Email (string), tag: 'email,required'

With reflect package we were able to extract basic information about our User struct, its type and kind and also list of all of its fields. As you can see, we also printed out a tag associated with each field. There's nothing magical about tags, field.Tag.Get method will return a string that matches the tag name (see docs) that we're free to do whatever we want.

To give you an idea how struct tags could be used to perform validations, i implemented a few validator types (numeric, string, email) using interface pattern. Here's the full runnable code sample:

package main

import (
  "fmt"
  "reflect"
  "regexp"
  "strings"
)

// Name of the struct tag used in examples.
const tagName = "validate"

// Regular expression to validate email address.
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)

// Generic data validator.
type Validator interface {
  // Validate method performs validation and returns result and optional error.
  Validate(interface{}) (bool, error)
}

// DefaultValidator does not perform any validations.
type DefaultValidator struct {
}

func (v DefaultValidator) Validate(val interface{}) (bool, error) {
  return true, nil
}

// StringValidator validates string presence and/or its length.
type StringValidator struct {
  Min int
  Max int
}

func (v StringValidator) Validate(val interface{}) (bool, error) {
  l := len(val.(string))

  if l == 0 {
    return false, fmt.Errorf("cannot be blank")
  }

  if l < v.Min {
    return false, fmt.Errorf("should be at least %v chars long", v.Min)
  }

  if v.Max >= v.Min && l > v.Max {
    return false, fmt.Errorf("should be less than %v chars long", v.Max)
  }

  return true, nil
}

// NumberValidator performs numerical value validation.
// Its limited to int type for simplicity.
type NumberValidator struct {
  Min int
  Max int
}

func (v NumberValidator) Validate(val interface{}) (bool, error) {
  num := val.(int)

  if num < v.Min {
    return false, fmt.Errorf("should be greater than %v", v.Min)
  }

  if v.Max >= v.Min && num > v.Max {
    return false, fmt.Errorf("should be less than %v", v.Max)
  }

  return true, nil
}

// EmailValidator checks if string is a valid email address.
type EmailValidator struct {
}

func (v EmailValidator) Validate(val interface{}) (bool, error) {
  if !mailRe.MatchString(val.(string)) {
    return false, fmt.Errorf("is not a valid email address")
  }
  return true, nil
}

// Returns validator struct corresponding to validation type
func getValidatorFromTag(tag string) Validator {
  args := strings.Split(tag, ",")

  switch args[0] {
  case "number":
    validator := NumberValidator{}
    fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
    return validator
  case "string":
    validator := StringValidator{}
    fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
    return validator
  case "email":
    return EmailValidator{}
  }

  return DefaultValidator{}
}

// Performs actual data validation using validator definitions on the struct
func validateStruct(s interface{}) []error {
  errs := []error{}

  // ValueOf returns a Value representing the run-time data
  v := reflect.ValueOf(s)

  for i := 0; i < v.NumField(); i++ {
    // Get the field tag value
    tag := v.Type().Field(i).Tag.Get(tagName)

    // Skip if tag is not defined or ignored
    if tag == "" || tag == "-" {
      continue
    }

    // Get a validator that corresponds to a tag
    validator := getValidatorFromTag(tag)

    // Perform validation
    valid, err := validator.Validate(v.Field(i).Interface())

    // Append error to results
    if !valid && err != nil {
      errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
    }
  }

  return errs
}

type User struct {
  Id    int    `validate:"number,min=1,max=1000"`
  Name  string `validate:"string,min=2,max=10"`
  Bio   string `validate:"string"`
  Email string `validate:"email"`
}

func main() {
  user := User{
    Id:    0,
    Name:  "superlongstring",
    Bio:   "",
    Email: "foobar",
  }

  fmt.Println("Errors:")
  for i, err := range validateStruct(user) {
    fmt.Printf("\t%d. %s\n", i+1, err.Error())
  }
}

On User struct we define a validation on Id field that should check if the value is in proper range (1-1000). Name field value is a string and validation should check the length. Bio field value is a string and we just require it to be present. And finally, Email field value should be a legit email address (at least formatted as email). Example User struct's field are all invalid and after running the code you will see the following output:

Errors:
  1. Id should be greater than 1
  2. Name should be less than 10 chars long
  3. Bio cannot be blank
  4. Email is not a valid email address

The main different between last example and the one before (where we used basic reflection of the type) is that we use reflect.ValueOf instead of reflectTypeOf. It's needed so we can grab the field value using v.Field(i).Interface() which gives us an interface that we can pass for validation. We can also get type of the field using v.Type().Field(i).

References

My particular example shows a basic use case, but there a plenty of other ways how to deal with interfaces and dynamic programming in Go. Here's a few open source projects that use struct tags extensively:

  • gorm - Database ORM for SQLite, Postgres and MySQL.
  • govalidator - Package of validators and sanitizers for strings, numerics, slices and structs.
  • mgo - The MongoDB database driver for Go.

Also checkout example code in gist.