All Articles

Simple yet Powerful Struct Validations In Go Without Reflection!

Introduction

Validations are needed to ensure that the structure of, well, a struct, is in the proper format your application requires. They can be used to check that a user's email is actually an email, or that a writer does not create a blog post without a title. There are many methods of implementing validations in golang. The most common method is to use simple if statements:

func (u *User) IsValid() error {
  if u.Email == "" {
    return errors.New("Email cannot be blank")
  }
  if len(u.Name) < 4 {
    return errors.New("Name cannot be less than 4 characters")
  }
}

The above code is repetitive, cumbersome, and probably not the best fit for most applications. An alternative method is to use struct tags and reflection. Packages such as go-playgrounds 'validator' use this method to allow you to write code like this:

type User struct {
    Email string `json:"email" validate:"required,email"`
    Name  string `json:"name" validate:"required,min=4,max=32"`
}

func main() {
  user := &User{}
  err := validate.Struct(user)
}

Using a library with struct tags comes with its own pros and cons. The go-playground validator package for example, is not that easy to customize. For simpler applications that do not want to add an additional dependency, or more complex applications who need flexibility, it is often easier to roll your own.

Probably the best validation framework is ActiveRecord::Validations, a module provided by Ruby on Rails. ActiveRecord allows you to write validation methods on a model, which are automatically run after every database transaction:

class User < ApplicationRecord
  validates_presence_of :name
  validates_uniqueness_of :email
end

Let's see how we can implement something similar in Golang.

The Validator Package

We can start by creating a simple struct called Validator. This struct will have one field; a slice of errors. That way we can return all the errors to the user at once:

package validator

// Validator : struct field validations
type Validator struct {
	Errors []error
}

Now, we can create a Validate method that receives a pointer to a Validator:

package validator

import "fmt"

...

// Validate :
func (v *Validator) Validate(cond bool, msg string, args ...interface{}) {
	if !cond {
		v.Errors = append(v.Errors, fmt.Errorf(msg, args...))
	}
}

The Validate method takes a condition, an error message, and an arbitrary number of arguments. The optional args are of type interface{}, an empty interface, so that it can accept generic types. If the condition is not true (ie: the resource is not valid), ]a formatted error message will be appended to the Validator struct.

We can use the Validate method to build commonly used validations. These methods will take the name of the field (for error messages), it's value, as well as other parameters required for each validation:

// ValidatePresenceOf : validates presence of struct string field
func (v *Validator) ValidatePresenceOf(fieldName string, fieldValue string) {
    // fieldValue must be greater than 0
	cond := len(strings.TrimSpace(fieldValue)) > 0
	v.Validate(cond, "%s cannot be blank", fieldName)
}

// ValidateMaxLengthOf : validates maximum character length of struct string field
func (v *Validator) ValidateMaxLengthOf(fieldName string, fieldValue string, max int) {
	// fieldValue must be less than maximum
    cond := len(fieldValue) < max
	v.Validate(cond, "%s cannot be greater than %d characters", fieldName, max)
}

// ValidateMinLengthOf : validates minimum character length of struct string field
func (v *Validator) ValidateMinLengthOf(fieldName string, fieldValue string, min int) {
	// fieldValue must be greater than minimum
    cond := len(fieldValue) > min
	v.Validate(cond, "%s must be at least %d characters", fieldName, min)
}

Validating User Input

Now that we finished the validator package, we can use it in http handlers to validate user input. We can create a validate method on our model/database struct:

package users

import (
  ...
  "github.com/username/appname/validator"
  ...
)
type User struct {
    Email string `json:"email"`
    Name  string `json:"name"`
}

func validate(u *User) []error {
	v := &validator.Validator{}
	v.ValidatePresenceOf("Email", u.Email)
    v.ValidatePresenceOf("Name", u.Name)
    v.ValidateMaxLengthOf("Email", u.Email, 32)
    v.ValidateMinLengthOf("Email", u.Email, 4)
	return v.Errors
}

We can also add a custom email regex validation using our Validate method:

func validate(u *User) []error {
  ...
  emailRegex := "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
  match, _ := regexp.MatchString(emailRegex, u.Email)
  // email must match regex
  v.Validate(match, "Email is not in valid format")
  ...
}

Now, in our handler, we simply have to call the validate method whenever we decode user input:

// POST "/users"
func Create(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  	user := new(User)
    json.NewDecoder(r.Body).Decode(&user)
    errs := validate(user)
    if errs != nil {
      w.WriteHeader(http.StatusUnprocessableEntity)
      json.NewEncoder(w).Encode(Stringify(errs))
      return
    }
}

If the slice of errors returned by the call to validate is not nil, meaning that the user provided invalid input, we can return an array of error messages along with a 422 status code. We can define the Stringify method in our validator module:

package validator

...
  
func Stringify(errs []error) []string {
  strErrors := make([]string, len(errs))
  for i, err := range errs {
    strErrors[i] = err.Error()
  }
  return strErrors
}

That's all for the Golang struct validations! The final code is available on github