Alex Alex - 18 days ago 6
JSON Question

How can I make use of offset value in enconding/json's UnmarshalTypeError for better error handling?

A little over a year ago, Go added an

Offset
value to the
json.UnmarshalTypeError
type (see closed issue here for context). The purpose behind the offset value makes sense, but I'm not sure how it can be used when reading a go http response body, which is of type
io.ReadCloser
.

// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError struct {
Value string // description of JSON value - "bool", "array", "number -5"
Type reflect.Type // type of Go value it could not be assigned to
Offset int64 // error occurred after reading Offset bytes
}


For example:

var body CustomType
decoderErr := json.NewDecoder(response.Body).Decode(&body)

if decoderErr != nil {

if typeError, ok := decoderErr.(*json.UnmarshalTypeError); ok {
// Do something with typeError.Offset here
}


}


At the point of the error getting caught, I've already read from
response.Body
via
json.NewDecoder...
. I'm looking for a way to read
response.Body
again, but only up to the point of the error by using the Offset value in typeError.

Answer

Since you want to reuse the request body you should read and store the body before you Unmarshal the body, then if there is a JSON syntax or type error you can return a more useful error using the body you previously stored.

Proof of concept:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

type Hello struct {
    Name    string `json:"name"`
    Message string `json:"message"`
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        b, err := ioutil.ReadAll(r.Body)
        if err != nil {
            http.Error(w, "Error reading body", 400)
            return
        }

        h := &Hello{}
        if err := json.Unmarshal(b, &h); err != nil {
            var msg string
            switch t := err.(type) {
            case *json.SyntaxError:
                jsn := string(b[0:t.Offset])
                jsn += "<--(Invalid Character)"
                msg = fmt.Sprintf("Invalid character at offset %v\n %s", t.Offset, jsn)
            case *json.UnmarshalTypeError:
                jsn := string(b[0:t.Offset])
                jsn += "<--(Invalid Type)"
                msg = fmt.Sprintf("Invalid value at offset %v\n %s", t.Offset, jsn)
            default:
                msg = err.Error()
            }
            http.Error(w, msg, 400)
            return
        }

        w.Write([]byte(`Good to go!`))
    })

    if err := http.ListenAndServe(":8000", nil); err != nil {
        log.Fatal(err)
    }
}