Gin: Unable to use Number validator

Created on 1 Sep 2017  路  13Comments  路  Source: gin-gonic/gin

Hi,

It's possible I just don't understand how to use the validations correctly, but I am trying to bind this form object:

type Form struct {
   Age int `binding:"required,number"`
}

but when testing supplying a string value in the form submission, it errors with an strconv.Atoi error instead of providing a validation error. It looks like it's failing when converting the data into my Form type, instead of first validating it.

Is there a recommended pattern for handling this? Using a interface{} or string for what should be an int field doesn't seem ideal.

Most helpful comment

Sorry for bumping this old and closed issue, but I'm running into the same problem here. I'm getting a strconv.ParseInt error, which doesn't tell me anything about which field failed validation (which is bad, because I want to show the users of my API which field was invalid).

From @benmoss:

The only options I see here would be to use an alternate parser like jsoniter or to wrap JSON errors in validation errors, trying to present a more uniform API of error types for the developer to handle.

I think exactly that is a very good idea, and would go a long way towards solving this problem .

All 13 comments

@benmoss Could you please provide the code you're trying to run?
Probably the best rule for Age is min=10,max=0 or you can simply use numeric which means you can use it like this:

type Form struct {
   Age int `binding:"required,numeric,min=18,max=40"`
}

Gin uses http://godoc.org/gopkg.in/go-playground/validator.v8 as its validator. You can find the complete rules and documentation here: https://godoc.org/gopkg.in/go-playground/validator.v8

Here's code that reproduces the problem, I believe:

type Login struct {
    User     string `form:"user" json:"user" binding:"required"`
    Password int    `form:"password" json:"password" binding:"required,number"`
}

func main() {
    router := gin.Default()

    // Example for binding JSON ({"user": "manu", "password": "123"})
    router.POST("/loginJSON", func(c *gin.Context) {
        var json Login
        if err := c.BindJSON(&json); err != nil {
            panic(err)
        }
        if json.User == "manu" && json.Password == 123 {
            c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
        } else {
            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
        }
    })

    // Listen and serve on 0.0.0.0:8080
    router.Run(":8080")
}

And here are my results from running it:
https://gist.github.com/benmoss/d66516bb70116f8504161c308084ea6b

@benmoss Have you tried what I told? numeric is the correct tag but you are using number.

@benmoss
If you change the follow code:

type Login struct {
    User     string `form:"user" json:"user" binding:"required"`
    Password int    `form:"password" json:"password" binding:"required,number"`
}

to:

type Login struct {
    User     string `form:"user" json:"user" binding:"required"`
    Password int    `form:"password" json:"password" binding:"required"`
}

right?

@aghasoroush @benmoss Please try the following solution.

package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
)

type Login struct {
    User     string `form:"user" json:"user" binding:"required"`
    Password int    `form:"password" json:"password" binding:"required,numeric"`
}

func main() {
    router := gin.Default()

    // Example for binding JSON ({"user": "manu", "password": "123"})
    router.POST("/loginJSON", func(c *gin.Context) {
        json := new(Login)
        if err := c.BindJSON(&json); err != nil {
            fmt.Println(err)
        }
        if json.User == "manu" && json.Password == 19 {
            c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
        } else {
            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
        }
    })

    // Listen and serve on 0.0.0.0:8080
    router.Run(":8080")
}

changes:

- var json Login
+ json := new(Login)

@appleboy this solution still gives you a json.UnmarshalTypeError rather than a validation error. What's failing is json.Unmarshal, and the numeric validation has no effect.

@appleboy benmoss is right.

json value has types, if your value is quoted in "
Then the std json lib cannot umarshal it to int value and report error
Details can be refered from json rfc:
https://tools.ietf.org/html/rfc7159

Form request value has no types, they are all passed by string in your http request body as user=abc&password=123. This is why your json request failed to bind but form request success

If you want to successfully bind Json to your struct, change Json to
{"user": "abc", "password":44323}

From
{"user":"abc","password":"44323"}

By the way, numeric tag only make sense when your value type is string, numeric validate on int value is meaningless

Also, if you want the Json lib convert type automatically, you could try jsoniter fuzzy mode

@cch123 yes, I think that for the JSON case the problem is that you need to unmarshal before validation can happen, but that Go's strict JSON parser will fail. The only options I see here would be to use an alternate parser like jsoniter or to wrap JSON errors in validation errors, trying to present a more uniform API of error types for the developer to handle.

I think the form data case is actually easier, since we can apply all the validations to the string types of the form data before unmarshaling them. Right now the unmarshaling happens first, resulting in a strconv errors in this case.

@benmoss , no matter which type(json/form/msgpack/pb) of binding you are using, bind behavior happens before validation, you can read the binding part of gin's code to confirm that. eg, form binding:

func (formPostBinding) Bind(req *http.Request, obj interface{}) error {
    if err := req.ParseForm(); err != nil {
        return err
    }
    if err := mapForm(obj, req.PostForm); err != nil { // ===> bind here
        return err
    }
    return validate(obj) // ===> validate here
}

The form type binding didn't fail, was just because form request data have no type, the form decoder will do the type conversion for you. But the json decoder will not.

Sorry for bumping this old and closed issue, but I'm running into the same problem here. I'm getting a strconv.ParseInt error, which doesn't tell me anything about which field failed validation (which is bad, because I want to show the users of my API which field was invalid).

From @benmoss:

The only options I see here would be to use an alternate parser like jsoniter or to wrap JSON errors in validation errors, trying to present a more uniform API of error types for the developer to handle.

I think exactly that is a very good idea, and would go a long way towards solving this problem .

Also running into this issue. This renders using the validator's translator almost pointless because there is still a need to dissect what was invalid with the request when it was malformed in this specific way. We want to use translations to provide user-friendly messages for _all_ input errors.

I too like the idea of standardising all of the errors coming out of the Bind functions as much as possible to ValidationErrors.

err := context.BindQuery(&p) is work
err := context.ShouldBindQuery(&p) is not work

Was this page helpful?
0 / 5 - 0 ratings

Related issues

olegsobchuk picture olegsobchuk  路  3Comments

ghost picture ghost  路  3Comments

rawoke083 picture rawoke083  路  3Comments

ccaza picture ccaza  路  3Comments

kekemuyu picture kekemuyu  路  3Comments