How to assert error type json.UnmarshalTypeError when caught by gin c.BindJSON

3.5k Views Asked by At

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" }

3

There are 3 best solutions below

1
On BEST ANSWER

[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)

  1. use errors.As
  2. since 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

6
On

errors.Is looks for an exact match (in your case, an empty instance of json.UnmarshalTypeError). Use errors.As instead:

var jsonErr *json.UnmarshalTypeError
if errors.As(e.Err, &jsonErr) {
    log.Println("Json binding error")
}

Playground example

However, as @LeGEC has pointed out, this assumes that gin's errors conform to the standard Unwrapper interface, which they don't.

0
On
  var (
       msg string
  )
  switch errs.(type) {
       case *json.UnmarshalTypeError:
           msg = errs.(*json.UnmarshalTypeError).Field + " type error."
       case validator.ValidationErrors:
           validationErrs := errs.(validator.ValidationErrors)
           for _, e := range validationErrs.Translate(trans) {
               msg = e
               break
           }
       default:
           msg = "unknow error"
  }