I'm trying to catch binding errors with gin gonic and it's working fine for all validation errors from go-playground/validator/v10 but i'm having an issue catching errors when unmarshalling into the proper data type.
Unsuccessful validation of a struct field will return a gin.ErrorTypeBind
Type of error when using validator tags ( required, ...)
but if i have a struct
type Foo struct {
ID int `json:"id"`
Bar string `json:"bar"`
}
And the json i'm trying to pass is of a wrong format (passing a string instead of a number for id )
{
"id":"string",
"bar":"foofofofo"
}
It will fail with an error json: cannot unmarshal string into Go struct field Foo.id of type int
It is still caught as a gin.ErrorTypeBind
in my handler as an error in binding but as i need to differentiate between validation error and unmarshalling error i'm having issues.
I have tried Type casting on validaton error doesn't work for unmarshalling :
e.Err.(validator.ValidationErrors)
will panic
or just errors.Is but this will not catch the error at all
if errors.Is(e.Err, &json.UnmarshalTypeError{}) {
log.Println("Json binding error")
}
My goal in doing so is to return properly formatted error message to the user. It's currently working well for all the validation logic but i can't seem to make it work for json data where incorrect data would be sent to me.
any ideas?
edit :
adding example to reproduce :
package main
import (
"encoding/json"
"errors"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type Foo struct {
ID int `json:"id" binding:"required"`
Bar string `json:"bar"`
}
func FooEndpoint(c *gin.Context) {
var fooJSON Foo
err := c.BindJSON(&fooJSON)
if err != nil {
// caught and answer in the error MW
return
}
c.JSON(200, "test")
}
func main() {
api := gin.Default()
api.Use(ErrorMW())
api.POST("/foo", FooEndpoint)
api.Run(":5000")
}
func ErrorMW() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
for _, e := range c.Errors {
switch e.Type {
case gin.ErrorTypeBind:
log.Println(e.Err)
var jsonErr json.UnmarshalTypeError
if errors.Is(e.Err, &jsonErr) {
log.Println("Json binding error")
}
// if errors.As(e.Err, &jsonErr) {
// log.Println("Json binding error")
// }
// in reality i'm making it panic.
// errs := e.Err.(validator.ValidationErrors)
errs, ok := e.Err.(validator.ValidationErrors)
if ok {
log.Println("error trying to cast validation type")
}
log.Println(errs)
status := http.StatusBadRequest
if c.Writer.Status() != http.StatusOK {
status = c.Writer.Status()
}
c.JSON(status, gin.H{"error": "error"})
default:
log.Println("other error")
}
}
if !c.Writer.Written() {
c.JSON(http.StatusInternalServerError, gin.H{"Error": "internal error"})
}
}
}
}
trying sending a post request with a body
{
"id":"rwerewr",
"bar":"string"
}
interface conversion: error is *json.UnmarshalTypeError, not validator.ValidationErrors
this will work :
{
"id":1,
"bar":"string"
}
and this will (rightfully ) return Key: 'Foo.ID' Error:Field validation for 'ID' failed on the 'required' tag
{ "bar":"string" }
[update] : sice version 1.7.0, which integrates this PR, it is now possible to use errors.Is
/ errors.As
on a gin.Error
.
So you can write :
err := c.BindJson(&fooJson)
if err != nil {
var jsErr *json.UnmarshalTypeError
if errors.As(err, &jsErr) {
fmt.Println("the json is invalid")
} else {
fmt.Println("this is something else")
}
}
[edit] : meh, it won't fix @Flimzy's answer : looking at the docs, gin.Error
doesn't implement the .Unwrap()
method.
You would have to first convert your error to a gin.Error
, then check ginErr.Err
:
// you can probably work with straight conversion from interface to target type :
if g, ok := err.(*gin.Error); ok {
if _, ok := g.Err.(*json.UnmarshalTypeError); ok {
log.Println("Json binding error")
}
}
// or use errors.As() :
var g *gin.Error
if errors.As(err, &g) {
var j *json.UnmarshalTypeError
if errors.As(g.Err, &j) {
log.Println("Json binding error")
}
}
my initial answer :
(fixing @Fllimzy's answer)
errors.As
json.UnmarshalTypeError
is a struct (not an interface), you have to explicitly disinguish between json.UnmarshalTypeError
and *json.UnmarshalTypeError
Try running :
var jsonErr *json.UnmarshalTypeError // emphasis on the '*'
if errors.As(e.Err, &jsonErr) {
log.Println("Json binding error")
}
Here is an illustration of how errors.As()
behaves :
https://play.golang.org/p/RVz6xop5k4u