Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to list all the required fields using gin-gonic #2811

Open
NishantG01 opened this issue Aug 4, 2021 · 11 comments
Open

Is there a way to list all the required fields using gin-gonic #2811

NishantG01 opened this issue Aug 4, 2021 · 11 comments

Comments

@NishantG01
Copy link

  • With issues:
    • Use the search tool before opening a new issue.
    • Please provide source code and commit sha if you found a bug.
    • Review existing issues and provide feedback or react to them.

Description

Generally, we create a struct and add tags for binding. If the binding key is not present gin-gonic will error out with generic message exposing the struct.
Is there a way that we can enlist all the required fields with there tags, and send specific error message pertaining to the field which is not present.

How to reproduce

package main

import (
	"github.com/gin-gonic/gin"
)
type Test struct {
	A string `json:"a" binding:"required"`
	B string `json:"b" binding:"required"`
}

func main() {
	g := gin.Default()
	g.GET("/test", func(c *gin.Context) {
		var t Test
		if err := c.ShouldBindJSON(&t); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, "ok")
	})
	g.Run(":9000")
}

Expectations

$ curl --location --request GET 'http://localhost:9000/test' \
--header 'Content-Type: application/json' \
--data-raw '{
    "a":""
}'
required:{a: string,b: string}

Actual result

$curl --location --request GET 'http://localhost:9000/test' \
--header 'Content-Type: application/json' \
--data-raw '{
    "a":""
}'
{
    "error": "Key: 'Test.A' Error:Field validation for 'A' failed on the 'required' tag\nKey: 'Test.B' Error:Field validation for 'B' failed on the 'required' tag"
}

Environment

  • go version:
  • gin version (or commit ref):
  • operating system:
@jimbirthday
Copy link

@NishantG01
I checked the documentation and it seems that I did not find the result you expected, but the documentation states
.

Behavior - These methods use ShouldBindWith under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.

image

Hope it helps you

@NishantG01
Copy link
Author

@jimbirthday,

As can be seen in the example, it very well sends the error, but it exposes the code internal struct. Instead I wanted the error to use the tags associated with struct along with listing all the required fields using gin.

@jimbirthday
Copy link

@NishantG01

I found a centralized method that may be able to meet your needs


type User struct {
	Email string `json:"email" binding:"required"`
	Name  string `json:"name" binding:"required"`
}
func main() {
	route := gin.Default()
	route.POST("/user", validateUser)
	route.Run(":8085")
}
func validateUser(c *gin.Context) {
	var u User
	if err := c.ShouldBindJSON(&u); err == nil {
		c.JSON(http.StatusOK, gin.H{"message": "User validation successful."})
	} else {
		var verr validator.ValidationErrors
		if errors.As(err, &verr) {
			c.JSON(http.StatusBadRequest, gin.H{"errors": Simple(verr)})
			return
		}
	}
}
func Simple(verr validator.ValidationErrors) map[string]string {
	errs := make(map[string]string)
	for _, f := range verr {
		err := f.ActualTag()
		if f.Param() != "" {
			err = fmt.Sprintf("%s=%s", err, f.Param())
		}
		errs[f.Field()] = err
	}
	return errs

curl -s -X POST http://localhost:8085/user \
    -H 'content-type: application/json' \
    -d '{"email1": "george@vandaley.com"}'
{"errors":{"Email":"required","Name":"required"}}%        

link: https://blog.depa.do/post/gin-validation-errors-handling

or

link: https://pkg.go.dev/github.com/go-playground/validator#RegisterTranslationsFunc

@NishantG01
Copy link
Author

NishantG01 commented Aug 6, 2021

@jimbirthday ,

Thanks for the example. Appreciate your help.
This example will fail if we give the wrong input, can we handle it in the same way that we handled the required fields.

curl --location --request POST 'http://localhost:8085/user' \
--header 'content-type: application/json' \
--data-raw '{
    "email": "me@example.com",
    "name": 1
}
    '
json: cannot unmarshal number into Go struct field User.name of type string
[GIN] 2021/08/06 - 14:27:31 | 200 |     201.807µs |             ::1 | POST     "/user"

Basically I am looking for the output in the format

{
    "errors": {
        "name": "required, string"
    }
}

Really Appreciate your help.
One request, if you can provide the complete import statement as well with the example it will ease out on running the program to validate. For now I have been able to run this, but it took me a long time to find the right imports to use.

@jimbirthday
Copy link

@NishantG01

this is my code :


import (
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
	"net/http"
)

type User struct {
	Email string `json:"email" binding:"required"`
	Name  string `json:"name" binding:"required"`
}
func main() {
	route := gin.Default()
	route.POST("/user", validateUser)
	route.Run(":8085")
}
func validateUser(c *gin.Context) {
	var u User
	if err := c.ShouldBindJSON(&u); err == nil {
		c.JSON(http.StatusOK, gin.H{"message": "User validation successful."})
	} else {
		var verr validator.ValidationErrors
		if errors.As(err, &verr) {
			c.JSON(http.StatusBadRequest, gin.H{"errors": Simple(verr)})
		}else {
			c.JSON(http.StatusBadRequest, gin.H{"errors": err.Error()})
		}
	}
}
func Simple(verr validator.ValidationErrors) map[string]string {
	errs := make(map[string]string)
	for _, f := range verr {
		err := f.ActualTag()
		if f.Param() != "" {
			err = fmt.Sprintf("%s=%s", err, f.Param())
		}
		errs[f.Field()] = err
	}
	return errs
}

You can see that in the source code, json decode is performed first, and then validated, I think you can achieve the function you want by modifying the source code

image

https://blog.depa.do/post/gin-validation-errors-handling

This article explains the use case of github.com/go-playground/validator very well

@NishantG01
Copy link
Author

@jimbirthday
I solved this using the below code, but would like to know your views, if it's possible to tackle it differently

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator/v10"
)

type User struct {
	Email string `json:"email" binding:"required"`
	Name  string `json:"name" binding:"required"`
}

func main() {
	route := gin.Default()
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			if name == "-" {
				return ""
			}
			return name
		})
	}
	route.POST("/user", validateUser)
	route.Run(":8085")
}
func validateUser(c *gin.Context) {
	var u User
	if err := c.ShouldBindJSON(&u); err == nil {
		c.JSON(http.StatusOK, gin.H{"message": "User validation successful."})
	} else {
		fmt.Println(err)
		var verr validator.ValidationErrors
		var jsErr *json.UnmarshalTypeError	

		if errors.As(err, &verr) {
			c.JSON(http.StatusBadRequest, gin.H{"errors": Simple(verr)})
			return
		} else if errors.As(err, &jsErr) {
			c.JSON(http.StatusBadRequest, gin.H{"errors": MarshalErr(*jsErr)})
			return
		}
	}
}
func MarshalErr(jsErr json.UnmarshalTypeError) map[string]string{
	errs := make(map[string]string)
	errs[jsErr.Field]=jsErr.Type.String()
	
	return errs
}
func Simple(verr validator.ValidationErrors) map[string]string {
	errs := make(map[string]string)
	for _, f := range verr {
		err := f.ActualTag()

		if f.Param() != "" {
			err = fmt.Sprintf("%s=%s", err, f.Param())
		}
		errs[f.Field()] = err + "," + f.Kind().String()
		fmt.Println(errs)
	}
	return errs

}

type ValidationError struct {
	Field  string `json:"field"`
	Reason string `json:"reason"`
}

func Descriptive(verr validator.ValidationErrors) []ValidationError {
	errs := []ValidationError{}

	for _, f := range verr {
		err := f.ActualTag()
		if f.Param() != "" {
			err = fmt.Sprintf("%s=%s", err, f.Param())
		}
		errs = append(errs, ValidationError{Field: f.Field(), Reason: err})
	}

	return errs
}

@jimbirthday
Copy link

@NishantG01
I am glad you solved this problem, you can encapsulate it in your favorite style

@NishantG01
Copy link
Author

@jimbirthday ,
The validation fails if we are binding array of struct. here is an example

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator/v10"
)

type User struct {
	Email string `json:"email" form:"e-mail" binding:"required"`
	Name  string `json:"name" binding:"required"`
}

func main() {
	route := gin.Default()
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			if name == "-" {
				return ""
			}
			return name
		})
	}
	route.POST("/user", validateUser)
	route.Run(":8085")
}
func validateUser(c *gin.Context) {
	var u []User
	if err := c.ShouldBindJSON(&u); err == nil {
		c.JSON(http.StatusOK, gin.H{"message": "User validation successful."})
	} else {
		fmt.Println(err)
		var verr validator.ValidationErrors
		var jsErr *json.UnmarshalTypeError

		if errors.As(err, &verr) {
			c.JSON(http.StatusBadRequest, gin.H{"errors": Descriptive(verr)})
			return
		} else if errors.As(err, &jsErr) {
			c.JSON(http.StatusBadRequest, gin.H{"errors": MarshalErr(*jsErr)})
			return
		}
	}
}

func Simple(verr validator.ValidationErrors) map[string]string {
	errs := make(map[string]string)
	for _, f := range verr {
		err := f.ActualTag()

		if f.Param() != "" {
			err = fmt.Sprintf("%s=%s", err, f.Param())
		}
		errs[f.Field()] = err + "," + f.Kind().String()
		fmt.Println(errs)
	}
	return errs

}

type ValidationError struct {
	Field  string `json:"field"`
	Type   string `json:"type"`
	Reason string `json:"reason"`
}
func MarshalErr(jsErr json.UnmarshalTypeError) ValidationError {
	errs := ValidationError{}
	errs.Field=jsErr.Field
	errs.Reason="invalid type"
	errs.Type=jsErr.Type.String()
	return errs
}
func Descriptive(verr validator.ValidationErrors) []ValidationError {
	errs := []ValidationError{}

	for _, f := range verr {
		fmt.Println(f)
		err := f.ActualTag()
		if f.Param() != "" {
			err = fmt.Sprintf("%s=%s", err, f.Param())
		}
		errs = append(errs, ValidationError{Field: f.Field(), Reason: err, Type: f.Type().Name()})
	}

	return errs
}

Test Input

curl --location --request POST 'http://localhost:8085/user' \
--header 'content-type: application/json' \
--data-raw '[
    {
        "email": "me@example.com",
        "name": "Me"
    },
    {
        "email": "me@example.com"
    }
]'

Expected output

"errors": [
        {
            "field": "name",
            "type": "string",
            "reason": "required"
        }
    ]

Can you please help me with it. Do you have any suggestion and pointer to the same. Appreciate your help.

@jimbirthday
Copy link

@NishantG01
I don't quite understand what you mean, I don't seem to have any problems with this result

@NishantG01
Copy link
Author

@jimbirthday ,
If you look at the second array which is part of the son, it doesn't have the field name in it. Ideally it should fail and return the error in the format that we were getting when the binding was not on an array of struct.
In this case

var u []User

@jimbirthday
Copy link

I understand it should be written like this, you try

type Ls struct {
	Users []*User `json:"users" binding:"required"`
}

type User struct {
	Email string `json:"email" form:"e-mail" binding:"required"`
	Name  string `json:"name" binding:"required"`
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants