56

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.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
adriaan.wiers
  • 715
  • 1
  • 6
  • 7
  • Also take a look at the [binding](https://github.com/mholt/binding#custom-data-validation) package; it facilitates struct validation. Struct tags can be useful but only to an extent. – Matt May 31 '14 at 14:53
  • 2
    `IsXXX` methods usually return `bool`, if you want just an error return I'd probably make it a verb (`Validate` maybe). Any particular reason why non-negative fields aren't just `uint`? – Dave C Mar 16 '15 at 01:34
  • Depending on how often you use the `Start, End time.Time` pair you could make your own time interval type (and deal with bad ranges there) and/or consider `Start time.Time` and `Duration time.Duration` (unless you really need two time.Location values in the times). – Dave C Mar 16 '15 at 01:37

9 Answers9

51

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
}
julienc
  • 19,087
  • 17
  • 82
  • 82
  • 40
    I wrote this package. Thanks for sharing it. One thing it cannot do that the original asker mentions is the ability to compare two fields (`e.End > e.Start`). I can see how that could be useful though. I think I might implement something like that. –  May 30 '14 at 19:04
  • Yes, please! That would be great. I am going to try thsi library – adriaan.wiers May 30 '14 at 19:08
  • 2
    Looks like @RobertoSelbach [added cross-field validation](https://github.com/go-playground/validator/blob/v9/doc.go#L47). – Xeoncross Nov 04 '17 at 00:35
23

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()
}
fabrizioM
  • 46,639
  • 15
  • 102
  • 119
  • 2
    About library limitations, this is something you will need to deal with with _any_ library, not just validation. That's why opensource is so cool, you can improve the library and share it with the world. That said, I like your approach too and I think it could become a useful package (which would run in eventual limitations but we've been through that.) –  May 30 '14 at 19:06
  • 1
    Sometimes that is true, however if proper options are provided by the library it won't have as many limitations as you might think, for example https://github.com/bluesuncorp/validator allows you to add your own custom functions when the provided ones don't meet your needs and then your function can be used right inside the library along side the built in validations. – deankarn Sep 12 '15 at 11:44
12

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

deankarn
  • 462
  • 2
  • 6
  • 17
  • 1
    Looks very promising and seems to be in active development compared to go-validator! +1 –  May 02 '15 at 16:48
9

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.

Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
  • This pattern is what we are using and it's really awesome as each model or entity can have it's own validator that anyone consuming it can use. – Arjun Patel May 25 '23 at 17:37
3

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)
}
RussellLuo
  • 155
  • 2
  • 9
1

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())
}
OneOfOne
  • 95,033
  • 20
  • 184
  • 185
  • This is going to construct all the error values each time Validate is called. You've also got the sense of the checks backwards. – Paul Hankin May 30 '14 at 20:26
  • Well, normally you will save the errors somewhere and use `Check(C{e.Id <= 0, errEtc}...)` instead – OneOfOne May 30 '14 at 23:18
1

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

Alternatively, a more Go-specific idiomatic solution is ozzo-validation.

FGM
  • 2,830
  • 1
  • 31
  • 31
0

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.

jimt
  • 25,324
  • 8
  • 70
  • 60
0

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