PHPFixing
  • Privacy Policy
  • TOS
  • Ask Question
  • Contact Us
  • Home
  • PHP
  • Programming
  • SQL Injection
  • Web3.0

Monday, November 14, 2022

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

 November 14, 2022     error-handling, go, go-gin, json, validation     No comments   

Issue

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



Answered By - LeGEC
Answer Checked By - Candace Johnson (PHPFixing Volunteer)
  • Share This:  
  •  Facebook
  •  Twitter
  •  Stumble
  •  Digg
Newer Post Older Post Home

0 Comments:

Post a Comment

Note: Only a member of this blog may post a comment.

Total Pageviews

Featured Post

Why Learn PHP Programming

Why Learn PHP Programming A widely-used open source scripting language PHP is one of the most popular programming languages in the world. It...

Subscribe To

Posts
Atom
Posts
Comments
Atom
Comments

Copyright © PHPFixing