Model Validation in Golang

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
- All built-in tags can be found on the official documentation page: https://pkg.go.dev/github.com/go-playground/validator/v10
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,omitnilvalidation 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?