Search code examples
jsonvalidationgoerror-handlinggo-gin

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


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


Solution

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