go-playground / validator

:100:Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving
MIT License
16.85k stars 1.32k forks source link

Question about single-dimension array with `required,dive,required` #748

Open tiagomelo opened 3 years ago

tiagomelo commented 3 years ago

Package version eg. v9, v10:

v10

Issue, Question or Enhancement:

Following the example provided,

// User contains user information
type User struct {
    FirstName      string     `json:"fname"`
    LastName       string     `json:"lname"`
    Age            uint8      `validate:"gte=0,lte=130"`
    Email          string     `json:"e-mail" validate:"required,email"`
    FavouriteColor string     `validate:"hexcolor|rgb|rgba"`
    Addresses      []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}

// Address houses a users address information
type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
    Planet string `validate:"required"`
    Phone  string `validate:"required"`
}

Why do I need to specify required,dive,required in Addresses? Not sure about the need of the second required, if I remove it the result will be the same if I miss any of the required fields in Address. What am I missing?

Code sample, to showcase or reproduce:

package main

import (
    "fmt"
    "reflect"
    "strings"

    "github.com/go-playground/validator/v10"
)

// User contains user information
type User struct {
    FirstName      string     `json:"fname"`
    LastName       string     `json:"lname"`
    Age            uint8      `validate:"gte=0,lte=130"`
    Email          string     `json:"e-mail" validate:"required,email"`
    FavouriteColor string     `validate:"hexcolor|rgb|rgba"`
    Addresses      []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}

// Address houses a users address information
type Address struct {
    Street string `validate:"required"`
    City   string `validate:"required"`
    Planet string `validate:"required"`
    Phone  string `validate:"required"`
}

// use a single instance of Validate, it caches struct info
var validate *validator.Validate

func main() {

    validate = validator.New()

    // register function to get tag name from json tags.
    validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
        name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
        if name == "-" {
            return ""
        }
        return name
    })

    // register validation for 'User'
    // NOTE: only have to register a non-pointer type for 'User', validator
    // internally dereferences during it's type checks.
    validate.RegisterStructValidation(UserStructLevelValidation, User{})

    // build 'User' info, normally posted data etc...
    address := &Address{
        Street: "Eavesdown Docks",
        Planet: "Persphone",
        Phone:  "none",
        City:   "Unknown",
    }

    user := &User{
        FirstName:      "",
        LastName:       "",
        Age:            45,
        Email:          "Badger.Smith@gmail",
        FavouriteColor: "#000",
        Addresses:      []*Address{address},
    }

    // returns InvalidValidationError for bad validation input, nil or ValidationErrors ( []FieldError )
    err := validate.Struct(user)
    if err != nil {

        // this check is only needed when your code could produce
        // an invalid value for validation such as interface with nil
        // value most including myself do not usually have code like this.
        if _, ok := err.(*validator.InvalidValidationError); ok {
            fmt.Println(err)
            return
        }

        for _, err := range err.(validator.ValidationErrors) {

            fmt.Println(err.Namespace()) // can differ when a custom TagNameFunc is registered or
            fmt.Println(err.Field())     // by passing alt name to ReportError like below
            fmt.Println(err.StructNamespace())
            fmt.Println(err.StructField())
            fmt.Println(err.Tag())
            fmt.Println(err.ActualTag())
            fmt.Println(err.Kind())
            fmt.Println(err.Type())
            fmt.Println(err.Value())
            fmt.Println(err.Param())
            fmt.Println()
        }

        // from here you can create your own error messages in whatever language you wish
        return
    }

    // save user to database
}

// UserStructLevelValidation contains custom struct level validations that don't always
// make sense at the field validation level. For Example this function validates that either
// FirstName or LastName exist; could have done that with a custom field validation but then
// would have had to add it to both fields duplicating the logic + overhead, this way it's
// only validated once.
//
// NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way
// hooks right into validator and you can combine with validation tags and still have a
// common error output format.
func UserStructLevelValidation(sl validator.StructLevel) {

    user := sl.Current().Interface().(User)

    if len(user.FirstName) == 0 && len(user.LastName) == 0 {
        sl.ReportError(user.FirstName, "fname", "FirstName", "fnameorlname", "")
        sl.ReportError(user.LastName, "lname", "LastName", "fnameorlname", "")
    }

    // plus can do more, even with different tag than "fnameorlname"
}
deankarn commented 3 years ago

Hey @tiagomelo, good question.

So the first required is applied on the array itself []*Address. So ensuring there is at least one value in the array. The second required is applied on each individual element within the array eg. *Address

The reason the example is like this is to show that you could have an array with one entry that is nil because *Address is a pointer.

Does that explain it?

empaguia84 commented 3 years ago

Screenshot from 2021-04-14 00-08-08 Am I calling it right?

the Addresses fields like street, planet, phone and city have already their value now, but the "dive" is not diving in that json array?

then... when I remove the said values in the addresses.. ok, it tells us all inside that json array are required,

Screenshot from 2021-04-14 00-08-08

but when, I put values in planet and city, still the "dive" is not diving to check which fields have already the value or not:

Screenshot from 2021-04-14 00-13-01

what's the problem here?

we have the same issue here https://stackoverflow.com/questions/62522747/golang-validator-with-custom-structs and I am landed here finding the solution...

deankarn commented 3 years ago

Hey @empaguia84 it looks like your JSON does not match your Go struct definitions.

The Go struct is expecting an Array of Addresses but your sending an JSON Object instead and so unmarshaling into those structs isn't happing.

You'll need to adjust your payload to something like:

{
  ...
  "Addresses": [{"street":"","planet":"",...}]
}
Oscar-nesto commented 1 year ago

@deankarn The dive validator does not work correctly. Here is a simple example of a required,dive,required

Created a struct of nested pointers to structs. Defined it as empty. The first required is not nil. Then it should dive into it and because there are no pointers in that slice it should fail on the second required. However, it passes with no errors. Thats because dive is not triggered for empty.

package main

import (
    "fmt"

    "github.com/go-playground/validator/v10"
)

type NestedStruct struct{}

type MyStruct struct {
    nested []*NestedStruct `validate:"required,dive,required"`
}

func main() {
    s := MyStruct{
        nested: []*NestedStruct{},
    }

    validate := validator.New()
    err := validate.Struct(s)

    fmt.Println(err) // should be error about missing nested items in the slice of pointers, but its nil
}

Go playground code: https://go.dev/play/p/Axsg3EA3g-F

The only way I accomplished the desired logic was to validate the slice length before diving into it

nested []*NestedStruct `validate:"required,min=1,dive,required"`
deankarn commented 1 year ago

This is functioning correctly @Oscar-nesto .

The first required, and any other tag before the dive, is applied at the slice level. After the dive the required or any other tag is validating against each element of the slice.

And so because there are no items in the slice, those validations have nothing to be applied to.

If your trying to ensure the slice is present and has at least one entry then add the gt=0 before the dive and after the required.