Idiomatic way to validate structs

ValidationGo

Validation Problem Overview


I need to validate that a struct value is correct and this means I need to check every field individually, which is easy for a small number of small structs but I was wondering if there's a better way to do it. Here's how I'm doing it right now.

type Event struct {
	Id     int
	UserId int
	Start  time.Time
	End    time.Time
	Title  string
	Notes  string
}

func (e Event) IsValid() error {
	if e.Id <= 0 {
		return errors.New("Id must be greater than 0")
	}
	if e.UserId <= 0 {
		return errors.New("UserId must be greater than 0")
	}
	if e.End <= e.Start {
		return errors.New("End must be after Start")
	}
	if e.Start < time.Now() {
		return errors.New("Cannot create events in the past")
	}
	if e.Title == "" {
		return errors.New("Title cannot be empty")
	}
	return nil
}

Is this the idiomatic way to validate the values of fields in a struct? It looks cumbersome.

Validation Solutions


Solution 1 - Validation

I don't see any other way to do this quickly. But I found a go package which can help you with this: https://github.com/go-validator/validator

The README file gives this example:

type NewUserRequest struct {
    Username string `validator:"min=3,max=40,regexp=^[a-zA-Z]$"`
    Name string     `validator:"nonzero"`
    Age int         `validator:"min=21"`
    Password string `validator:"min=8"`
}

nur := NewUserRequest{Username: "something", Age: 20}
if valid, errs := validator.Validate(nur); !valid {
    // values not valid, deal with errors here
}

Solution 2 - Validation

Doing that way you will end up writing a lot of duplicate code for each of your model.

Using a library with tags comes with its own pros and cons. Sometimes is easy to start but down the road you hit the library limitations.

One possible approach is to create a "Validator" that its only responsibility is to keep track of the possible errors inside an object.

A very approximate stub of this idea:

http://play.golang.org/p/buBUzk5z6I

package main

import (
	"fmt"
	"time"
)

type Event struct {
	Id     int
	UserId int
	Start  time.Time
	End    time.Time
	Title  string
	Notes  string
}

type Validator struct {
	err error
}

func (v *Validator) MustBeGreaterThan(high, value int) bool {
	if v.err != nil {
		return false
	}
	if value <= high {
		v.err = fmt.Errorf("Must be Greater than %d", high)
		return false
	}
	return true
}

func (v *Validator) MustBeBefore(high, value time.Time) bool {
	if v.err != nil {
		return false
	}
	if value.After(high) {
		v.err = fmt.Errorf("Must be Before than %v", high)
		return false
	}
	return true
}

func (v *Validator) MustBeNotEmpty(value string) bool {
	if v.err != nil {
		return false
	}
	if value == "" {
		v.err = fmt.Errorf("Must not be Empty")
		return false
	}
	return true
}

func (v *Validator) IsValid() bool {
	return v.err != nil
}

func (v *Validator) Error() string {
	return v.err.Error()
}

func main() {
	v := new(Validator)
	e := new(Event)
	v.MustBeGreaterThan(e.Id, 0)
	v.MustBeGreaterThan(e.UserId, 0)
	v.MustBeBefore(e.End, e.Start)
	v.MustBeNotEmpty(e.Title)
	if !v.IsValid() {
		fmt.Println(v)
	} else {
	fmt.Println("Valid")
	}
}
package main

import (
	"fmt"
	"time"
)

type Event struct {
	Id     int
	UserId int
	Start  time.Time
	End    time.Time
	Title  string
	Notes  string
}

type Validator struct {
	err error
}

func (v *Validator) MustBeGreaterThan(high, value int) bool {
	if v.err != nil {
		return false
	}
	if value <= high {
		v.err = fmt.Errorf("Must be Greater than %d", high)
		return false
	}
	return true
}

func (v *Validator) MustBeBefore(high, value time.Time) bool {
	if v.err != nil {
		return false
	}
	if value.After(high) {
		v.err = fmt.Errorf("Must be Before than %v", high)
		return false
	}
	return true
}

func (v *Validator) MustBeNotEmpty(value string) bool {
	if v.err != nil {
		return false
	}
	if value == "" {
		v.err = fmt.Errorf("Must not be Empty")
		return false
	}
	return true
}

func (v *Validator) IsValid() bool {
	return v.err != nil
}

func (v *Validator) Error() string {
	return v.err.Error()
}

func main() {
	v := new(Validator)
	e := new(Event)
	v.MustBeGreaterThan(e.Id, 0)
	v.MustBeGreaterThan(e.UserId, 0)
	v.MustBeBefore(e.End, e.Start)
	v.MustBeNotEmpty(e.Title)
	if !v.IsValid() {
		fmt.Println(v)
	} else {
	fmt.Println("Valid")
	}
}

You can then create your Validate method and use the same code:

func (e *Event) IsValid() error {
        v := new(Validator)
	v.MustBeGreaterThan(e.Id, 0)
	v.MustBeGreaterThan(e.UserId, 0)
	v.MustBeBefore(e.End, e.Start)
	v.MustBeNotEmpty(e.Title)
	return v.IsValid()
}

Solution 3 - Validation

To help anyone else that may be looking for another validation library I created the following https://github.com/bluesuncorp/validator

It addresses some issues that other plugins have not implemented yet that others in this thread had mentioned such as:

  • Returning all validation errors
  • multiple validations per field
  • cross field validation ex. Start > End date

Inspired by several other projects including the accepted answer of go-validator/validator

Solution 4 - Validation

I'd write explicit code rather than use a validation library. The advantage of writing your own code is that you don't add an extra dependency, you don't need to learn a DSL, and you can check properties of your structs that are dependent on multiple fields (for example, that start < end).

To cut down on the boilerplate, I might extract a function that adds an error message to a slice of errors in the case an invariant is false.

func check(ea *[]string, c bool, errMsg string, ...args) {
    if !c { *ea = append(*ea, fmt.Sprintf(errMsg, ...args)) }
}

func (e *Event) Validate() error {
    var ea []string
    check(&ea, e.ID >= 0, "want positive ID, got %d", e.ID)
    check(&ea, e.Start < e.End, "want start < end, got %s >= %s", e.Start, e.End)
    ...
    if len(ea) > 0 {
        return errors.New(strings.Join(ea, ", "))
    }
    return nil
 }

This returns all ways the struct fails validation rather than just the first, which may or may not be what you want.

Solution 5 - Validation

Maybe you can give validating a try. With this library, you can validate your struct like this:

package main

import (
	"fmt"
	"time"

	v "github.com/RussellLuo/validating"
)

type Event struct {
	Id     int
	UserId int
	Start  time.Time
	End    time.Time
	Title  string
	Notes  string
}

func (e *Event) Schema() v.Schema {
	return v.Schema{
		v.F("id", &e.Id):          v.Gt(0),
		v.F("user_id", &e.UserId): v.Gt(0),
		v.F("start", &e.Start):    v.Gte(time.Now()),
		v.F("end", &e.End):        v.Gt(e.Start),
		v.F("title", &e.Title):    v.Nonzero(),
		v.F("notes", &e.Notes):    v.Nonzero(),
	}
}

func main() {
	e := Event{}
	err := v.Validate(e.Schema())
	fmt.Printf("err: %+v\n", err)
}

Solution 6 - Validation

A different approach that doesn't need reflection and returns on the first error is using something like this :

type Event struct {
	Id     int
	UserId int
	Start  time.Time
	End    time.Time
	Title  string
	Notes  string
}

func (e *Event) Validate() error {
	return Check(
		Cf(e.Id <= 0, "Expected ID <= 0, got %d.", e.Id),
		Cf(e.Start.UnixNano() > e.End.UnixNano(), "Expected start < end, got %s >= %s.", e.Start, e.End),
	)
}

type C struct {
	Check bool
	Error error
}

func Cf(chk bool, errmsg string, params ...interface{}) C {
	return C{chk, fmt.Errorf(errmsg, params...)}
}

func Check(args ...C) error {
	for _, c := range args {
		if !c.Check {
			return c.Error
		}
	}
	return nil
}

func main() {
	a := Event{Id: 1, Start: time.Now()}
	b := Event{Id: -1}
	fmt.Println(a.Validate(), b.Validate())
}

Solution 7 - Validation

The method you describe is certainly the most straight forward way to do it.

You can use reflection with struct field tags to do automated validation. But this will require writing a library which does this for you. The upside is that once you've written the validation library, you can reuse it with any struct.

An example of a way to use this code would be:

type Person struct {
    Name string `minlength:"3" maxlength:"20"`
    Age  int    `min:"18" max:"80"`
}

You would create an instance of this type and pass it into your validation code. It would use the rules in the field tags to validate the field values.

There are probably a few libraries out there which do this sort of thing for you, but I am not sure how well they work or if they are still being maintained.

Solution 8 - Validation

I think this is a better way!

import (
	"fmt"

	"github.com/bytedance/go-tagexpr/validator"
)

func Example() {
	var vd = validator.New("vd")

	type InfoRequest struct {
		Name string `vd:"($!='Alice'||(Age)$==18) && regexp('\\w')"`
		Age  int    `vd:"$>0"`
	}
	info := &InfoRequest{Name: "Alice", Age: 18}
	fmt.Println(vd.Validate(info) == nil)
}

https://github.com/bytedance/go-tagexpr/tree/master/validator

Solution 9 - Validation

I would suggest you look up the Cue Go integration, which covers many validation scenarios, and is especially interesting in that it is an abstract validator mapping to various languages, thereby allowing you to share validations among various parts of your system regardless of language, avoiding the issues with diverging language-based validators. Think, for example, of sharing your validators among JS and Go code

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionadriaan.wiersView Question on Stackoverflow
Solution 1 - ValidationjuliencView Answer on Stackoverflow
Solution 2 - ValidationfabrizioMView Answer on Stackoverflow
Solution 3 - ValidationdeankarnView Answer on Stackoverflow
Solution 4 - ValidationPaul HankinView Answer on Stackoverflow
Solution 5 - ValidationRussellLuoView Answer on Stackoverflow
Solution 6 - ValidationOneOfOneView Answer on Stackoverflow
Solution 7 - ValidationjimtView Answer on Stackoverflow
Solution 8 - ValidationHenry LeeView Answer on Stackoverflow
Solution 9 - ValidationFGMView Answer on Stackoverflow