go-playground / validator

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

Preventing struct level validation when field level validation fails #1149

Closed eddiedane closed 10 months ago

eddiedane commented 11 months ago

Package version

v10.15.0

Issue:

Disclaimer: am fairly new to go-playground/validator

When using field level validation with registered struct validation, it seems both validation level always run which is kind of unnecessary an can lead to unexpected results, like in my current application of these two validation level for login system.

Streamlined Code sample, to showcase or reproduce:


type LoginUserDto struct {
    Email string `validate:"required"`
    Password string `validate:"required"`
    Remember boolean `validate:"required"`
}

func main() {
    validate.RegisterStructValidation(verifyLoginCredentials, LoginUserDto{})
}

func verifyLoginCredentials(sl validator.StructLevel) {
    loginData := sl.Current().Interface().(LoginUserDto)

    // query database for user with loginData.Email
    var user string

    if user == nil {
        sl.ReportError(loginData.Email, "Email", "Email", "exists", "")
        return
    }

    // compare loginData.Password with user.HashedPassword
    passwordMatched := Compare( loginData.Password, user.HashedPassword )

    if !passwordMatched {
        sl.ReportError(loginData.Password, "Password", "Password", "checkpassword", "")
        return
    }
}

Observations

Now regardless of whether the Email or the Password field fails the field level validation (required i.e they have no value), the struct level validation still executes which very unnecessary and expensive, I also observed that the order of validation is field level first the struct level, which is awesome!

Question

Given that the field level validation executes before the struct level, is there a way to halt the execution of the struct level validations, in cases where the struct level requires/assumes the field level validation to pass

deankarn commented 11 months ago

@eddiedane not currently, there is no way to early stop validation in this specific way.

There is a way on fields of a struct that are a struct using the structonly tag, but needs to be a field of a struct in order to have a tag registered.

Usually when people are doing Struct Level validation are not also doing field level using tags that I'm aware of and would validate in the struct level manually. eg.

type LoginUserDto struct {
    Email    string
    Password string
    Remember bool
}

func main() {
    validate.RegisterStructValidation(verifyLoginCredentials, LoginUserDto{})
}

func verifyLoginCredentials(sl validator.StructLevel) {
    loginData := sl.Current().Interface().(LoginUserDto)

    // validate is email, not blank etc. etc..
    if loginData.Email == "" {
        sl.ReportError(loginData.Email, "Email", "Email", "exists", "")
        return
    }
    // validate other fields.

    // query database for user with loginData.Email
    var user string

    if user == nil {
        sl.ReportError(loginData.Email, "Email", "Email", "exists", "")
        return
    }

    // compare loginData.Password with user.HashedPassword
    passwordMatched := Compare(loginData.Password, user.HashedPassword)

    if !passwordMatched {
        sl.ReportError(loginData.Password, "Password", "Password", "checkpassword", "")
        return
    }
}

The other option would be to to use field level validations without struct level as all the database etc validation can also be done there as it has access to the parent struct. eg.

v := validator.New()
    v.RegisterValidation("password", func(fl validator.FieldLevel) bool {
        loginData := fl.Parent().Interface().(LoginUserDto)

        // query database for user with loginData.Email
        var user string
        if user == nil {
            return false
        }

        // compare loginData.Password with user.HashedPassword
        passwordMatched := Compare(loginData.Password, user.HashedPassword)

        if !passwordMatched {
            return false
        }
        return false
    })

I hope that provides some guidance.

eddiedane commented 10 months ago

Awesome @deankarn, thanks for your response, I guess using one of the validation levels will do for now, I was following the example on struct level validation, it included both field level and struct level validation, maybe it should be updated to avoid confusion.