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.
Comment From: sonemaro
@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
Comment From: benmoss
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
Comment From: sonemaro
@benmoss Have you tried what I told? numeric
is the correct tag but you are using number
.
Comment From: thinkerou
@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?
Comment From: appleboy
@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)
Comment From: benmoss
@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.
Comment From: sonemaro
@appleboy benmoss is right.
Comment From: cch123
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
From
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
Comment From: benmoss
@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.
Comment From: cch123
@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.
Comment From: torkelrogstad
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 .
Comment From: ericleb010
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
.
Comment From: OC0311
err := context.BindQuery(&p) is work err := context.ShouldBindQuery(&p) is not work
Comment From: appcypher
@benmoss Can this issue be reopened since this problem hasn't been resolved? I will have to create a new one otherwise.
BTW, was anyone able to find a workaround for this? I would really hate to send cryptic error messages to my users.
Comment From: qbiqing
Same, will be helpful to have ValidationErrors
for strconv
type errors
Comment From: Mohanbarman
@appcypher you can change the type of int
field to string
then number
binding will work as expected. You can convert the string to int afterwards. Here's an example
type RegisterDTO struct {
Age string `json:"limit" form:"limit" binding:"number"`
}
Comment From: shushenghong
please reopen, if a field defined as int, and client send a string like "abc", it will comeout strconv.ParseInt: parsing "abc": invalid syntax
, not validation error
Comment From: shushenghong
i suggest if a field parse value fail, set to the zero value of the golang type, like int default to 0, then we can add other validate tags such as min
max
ne
to validate
Comment From: jackbaron
@shushenghong you got a solution for this?
Comment From: shushenghong
no, i will try to send a PR for this latter
Comment From: Oranzh
Please reopen this issue!
Comment From: emarais-godaddy
A slight workaround for this is to switch on error types. Pass int he universal translator and the errors from ShouldBind
.
type ListRequest struct {
Limit int `form:"limit,default=20" binding:"numeric,gt=0,lte=100"`
Sort string `form:"sort,default=created_at" binding:"db_field"`
Order string `form:"order,default=desc" binding:"oneof=asc desc"`
Page int `form:"page,default=1" binding:"numeric,gt=0"`
}
if err := c.ShouldBind(&req); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, response.ValidationErrorResponse{
Status: response.StatusValidation,
Errors: request.ParseBindErrors(err),
})
return
}
type ValidationError struct {
Message string `json:"message" example:"Field can only contain alphabetic characters"`
FieldName string `json:"field_name,omitempty" example:"snake_case_field_name"`
}
func ParseBindErrors(err error, trans ut.Translator) []ValidationError {
var errs []ValidationError
switch v := err.(type) { //nolint:errorlint
case validator.ValidationErrors:
// For internal gin validation errors from the bound request context, we can hook into the translation
// for better error messages
validationErrs := err.(validator.ValidationErrors) //nolint:errorlint
for _, e := range validationErrs {
validationError := ValidationError{
Message: e.Translate(trans),
FieldName: strcase.ToSnake(e.Field()),
}
errs = append(errs, validationError)
}
case *strconv.NumError:
errs = []ValidationError{
{
Message: fmt.Errorf("error validating request, failed to convert: %w", err).Error(),
},
}
default:
// Default to a generic error response
errs = []ValidationError{
{
Message: fmt.Errorf("error validating request: %w", err).Error(),
},
}
}
return errs
}
Even just adding a default statement will help with the untranslated validation error.
Comment From: egege
Is there a solution now? I am currently using version 1.10
Comment From: QingShan-Xu
Same question