Skip to main content

Command Palette

Search for a command to run...

Model Validation in Golang

Updated
6 min read
Model Validation in Golang
D

Senior software developer, math, physics and engineering believer, seeking the truth and enjoying life with you <3 Top russian student in the past. Top 1000 on leetcode. Graduated from HSE University, FCS AMI 'Distributed Systems specialization (#150 world ranking)

Model validation is a crucial part of any piece of software. With proper validation you provide some useful guarantees and therefore simplify your further work. Working under assumptions also makes your code cleaner, because you don't need to make additional checks every time you use the model anymore - you just perceive the model to be completely valid. Probably every language has a community library that already solves the validation problem, so you don't have to worry about implementing your own solution. In this post we will talk about model validation in Golang, best practices and particularly about validator v10 library.

Validation Library - validator v10

There are many Golang libraries you can use for validating your structs. Many of them are listed there: Validation - Awesome Go. There is no much difference between them so I suggest choosing the one with the largest community, because this implies that it has a lot of features, there are no obvious bugs and it will be supported longer. For Golang it's a validator library that has already become the de facto standard for struct validation.

Validator library provides a fairly complete list of features. Most importantly, it gives you the ability to validate your structs using field tags and by writing custom validation functions that take the struct value and report all validation errors found. It works as simple as that:

var validate = validator.New(validator.WithRequiredStructEnabled())
​
type Movie struct {
  Title string `validate:"required"`
}
​
func main() {
  m := Movie{
    Title: "",
  }
  err := validate.Struct(m)
  fmt.Println(err) // Error: empty title
}

It's also worth to mention that gin framework is integrated with validator library and validation will be automatically applied while binding request into structs.

Now let's take a look at practical examples and how validator library helps us to solve them.

Field Validation with Tags

First and the most simple validation option requires to mark struct fields with relevant validation tags. By default, all validation tags are defined under validate tag name. Tag name can be changed using SetTagName method in the following way:

validate := validator.New(validator.WithRequiredStructEnabled())
validate.SetTagName("YOUR_TAG_NAME")

Then you need to define your model with validation tags and call Struct method:

var validate = validator.New(validator.WithRequiredStructEnabled())
​
type Movie struct {
  Title string `validate:"required"`
}
​
func main() {
  m := Movie{
    Title: "",
  }
  err := validate.Struct(m)
  fmt.Println(err) // Error: empty title
}

Multiple Tags

When you need multiple validators simply split them with comma (,):

type Movie struct {
  Title string `validate:"required,alpha"`
}
// Title: "abc" - Pass
// Title: ""    - Fail
// Title: "λ"   - Fail

Multiple Tags with OR

You can also concatenate validators with logical OR using pipe (|):

type Movie struct {
  Title string `validate:"required|alpha"`
}
// Title: "abc" - Pass
// Title: ""    - Fail
// Title: "λ"   - Pass

Multiple Tags with AND and OR

You can combine AND and OR and OR will be resolved first:

type Movie struct {
  Title string `validate:"number|alpha,lowercase"`
  // the same as:
  Title string `validate:"lowercase,number|alpha"`
}
// Title "a"  - Pass  // (TRUE)  AND (FALSE OR TRUE)
// Title "1"  - Pass  // (TRUE)  AND (TRUE  OR FALSE)
// Title "a1" - Fail  // (TRUE)  AND (FALSE OR FALSE)
// Title "A"  - Fail  // (FALSE) AND (FALSE OR TRUE)

Cross-Field Tags

It's even possible to perform a cross-field validation using tags. Take a look:

type Movie struct {
  Title string `validate:"eqfield=Name"`
  Name  string
}
// Title ""  Name ""  - Pass
// Title "a" Name "b" - Fail
// Title "a" Name "a" - Pass

And even more magic...

type Movie struct {
  Title string `validate:"eqcsfield=Info.Name"`
  Info  struct {
    Name string
  }
}
// Title ""  Name ""  - Pass
// Title "a" Name "b" - Fail
// Title "a" Name "a" - Pass

Custom Field Validation

You can define your own tags the following way:

func ValidateIsValidTag(fl validator.FieldLevel) bool {
  if fl.Field().Kind() == reflect.String {
    return fl.Field().String() == "valid"
  }
  return false
}
​
validate.RegisterValidation("is_valid", ValidateIsValidTag)

And then...

type Movie struct {
  Title string `validate:"is_valid"`
}
​
// Title ""        - Fail
// Title "valid"   - Pass
// Title "invalid" - Fail

Custom Struct Validation

For complex validation it's more recommended to use custom validation functions instead of putting dozens of validation tags with a complex boolean logic (AND, OR operators). This way your code is cleaner and easier to comprehend and extend.

Custom struct validation is a function that accepts struct value and returns a validation error if such occured. Here is an example:

type Movie struct {
  Title               string
  ReleaseYear         int
  ReleasedCurrentYear bool
}
​
func CustomValidateMovie(sl validator.StructLevel) {
  movie := sl.Current().Interface().(Movie)
  if movie.ReleasedCurrentYear != (movie.ReleaseYear == 2024) {
    sl.ReportError(movie.ReleaseYear, "release_year", "ReleaseYear", "release_info", "")
    sl.ReportError(movie.ReleasedCurrentYear, "released_cur_year", "ReleasedCurrentYear", "release_info", "")
  }
}
​
validate.RegisterStructValidation(CustomValidateMovie, Movie{})
​
// Year "2024" CurYear "true"  - Pass
// Year "2024" CurYear "false" - Fail
// Year "2000" CurYear "true"  - Fail

Best Practices

In this section I offer two pieces of advice that may not seem obvious in the beginning.

Fail Fast

Fail fast is a methodology that advices to fail (finish) your program as soon as fault (unresolvable/unexpected error) occurs and do not attempt to continue the work. It might sound weird to make your program give up each time it encounters an unexpected error, but when you try to debug the system that doesn't fail immediately after getting faulty it becomes clear why fail fast really pays off as a methodology. Practically, I recommend to do not continue model validation if some check has already failed. It's better to report about the error found as soon as possible indicating the primary issue properly.

Avoid Global State

Probably most of you already know that it's not a good practice to have global variables since it's difficult to manage them and to keep track of every change that happens with them. The same applies to var validate *validator.Validate — (usually) a global variable. I would recommend wrapping validation inside each module into function that should be called to validate any model from this module. It look like this:

package models
​
import (
  "fmt""github.com/go-playground/validator/v10"
)
​
var validate *validator.Validate
​
func init() {
  validate = validator.New(validator.WithRequiredStructEnabled())
}
​
type Movie struct {
  Title string
}
​
func Validate(obj interface{}) error {
  return validate.Struct(obj)
}
​
func main() {
  m := Movie{}
  err := Validate(m)
  fmt.Println(err)
}

Additional

I didn't mention, but

  • validator object (validator.Validate) is thread-safe

  • (obvious, but) validation works recursively and goes through all substructs

  • validation can access only exported methods and fields (ones starting with capital letters)

  • multiple validation tags are processed in the order defined

  • validation can be skipped on empty/nil values with omitempty, omitnil validation tags

It's really hard to cover all the features that validator library provides. And there is no such need. I've given you everything to make you feel comfortable when you start using it.

Summary

For model validation in Golang I highly recommend using validator v10 library with vast number of features and built-in validation options. I've shown in practice how to use the library and gave particularly useful pieces of advice of how to get the most of it without burdening your system. If you have any questions or something in the article was unclear, please feel free to leave it in the comments.

After all, will you use the validator v10 library? Do you like its design (tags, how custom validation works, etc)? If no, then what problems have you found and do you see a better approach that can solve these problems?