go-playground / validator

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

Question: How to validate that an inner slice of structs conforms to custom validation logic #923

Open LeviMatus opened 2 years ago

LeviMatus commented 2 years ago

Package version eg. v9, v10:

v10

Issue, Question or Enhancement:

Given two struct types, one of which holds a slice of the other as one of its members, how do I ensure that only one element of the slice has its value set to a target value (lets assume boolean field set to true). See the code sample for what I hope is a clearer illustration of what I'm asking here.

Example Scenario: Assume there's a need for a User to have a slice of Addresses. User's may have one Address with an IsMailing flag set to true. If two of such flags are set to true, then validation must fail. User's may have an arbitrary amount of IsMailing flags set to false.

I've laid out a few of the approaches I've tried. If there's a clean way of doing what I'm asking with reasonable control over errors, then please let me know.

Code sample, to showcase or reproduce:

type User struct {
    FirstName string
    LastName string
    Address []Address `validate:"required"`
}

type Address struct {
    Street string `validate:"required"`
    City string `validate:"required"`
    Planet string `validate:"required"`
    Phone string `validate:"required"`
    IsMailing bool `validate:"required"`
}

Moreover, []Address may need to be validated with other structs, so I want to be able to validate these rules independently from the User struct.

I've tried to accomplish this using:

type User struct {
    FirstName string
    LastName string
    Address AddressSliceWrapper
}

type Address struct {
    IsMailing bool `validate:"required"`
}

type Addresses []Address

// AddressSliceWrapper embeds the alias type for the sake of embedding a slice.
// This is very heavy handed.
type AddressSliceWrapper struct {
    Addresses
}
func main() {
    validate := validator.New()
    validate.RegisterCustomTypeFunc(validateMailingAddress, []Address{})

   // .... setup user and validate
}

func validateMailingAddress(field reflect.Value) interface{} {
    addresses, ok := field.Interface().([]Address)
    if !ok {
        return nil
    }

    var hasMailing bool
    for _, addr := range addresses {
        if addr.IsMailing && hasMailing {
            // for brevity. imagine a type which
            // implements validator.FieldError is used
            // so that validator.ValidationErrors may be returned.
            return errors.New("already has mailing address")
        }
        hasMailing = addr.IsMailing
    }

    return addresses
}

This would work if the error were honored, but when the error is returned, its treated as a success.

zxxf18 commented 2 years ago

add "dive"

type User struct {
    FirstName string
    LastName string
    Address []Address `validate:"required,dive"`
}

type Address struct {
    Street string `validate:"required"`
    City string `validate:"required"`
    Planet string `validate:"required"`
    Phone string `validate:"required"`
    IsMailing bool `validate:"required"`
}
fabiante commented 10 months ago

dive fixes my similar problem.

Is there explicit documentation on what dive does? The readme has no explaination on what is does besides: Ability to dive into both map keys and values for validation

deankarn commented 10 months ago

Yes @fabiante there is documentation here in the Go documentation https://pkg.go.dev/github.com/go-playground/validator/v10#hdr-Dive

Reminder to everyone the README for projects is not documentation, but rather a quick reference to information.