6

Let's say I have the following Go struct on the server

type account struct {
    Name    string
    Balance int
}

I want to call json.Decode on the incoming request to parse it into an account.

    var ac account
    err := json.NewDecoder(r.Body).Decode(&ac)

If the client sends the following request:

{
    "name": "test@example.com", 
    "balance": "3"
}

Decode() will return the following error:

json: cannot unmarshal string into Go value of type int

Now it's possible to parse that back into "you sent a string for Balance, but you really should have sent an integer", but it's tricky, because you don't know the field name. It also gets a lot trickier if you have a lot of fields in the request - you don't know which one failed to parse.

What's the best way to take that incoming request, in Go, and return the error message, "Balance must be a string", for any arbitrary number of integer fields on a request?

Kevin Burke
  • 61,194
  • 76
  • 188
  • 305
  • I guess you could do something like decode to a generic struct full of interfaces and use type assertion to see if all the int fields look like ints. – Adam Smith Feb 09 '16 at 16:51
  • 3
    There's an open issue to try and get more information from decode errors, but it's not trivial, and wouldn't happen until at least go1.7. Can you just implement json.Unmarshaler and validate the fields? – JimB Feb 09 '16 at 16:56
  • 1
    Ultimately: if the client sends a bad request, you should return a 400 Bad Request message and make your API documentation available so they know how it's wrong. – Adam Smith Feb 09 '16 at 17:11

3 Answers3

3

You can use a custom type with custom unmarshaling algorythm for your "Balance" field.

Now there are two possibilities:

  • Handle both types:

    package main
    
    import (
        "encoding/json"
        "fmt"
        "strconv"
    )
    
    type Int int
    
    type account struct {
        Name    string
        Balance Int
    }
    
    func (i *Int) UnmarshalJSON(b []byte) (err error) {
        var s string
        err = json.Unmarshal(b, &s)
        if err == nil {
            var n int
            n, err = strconv.Atoi(s)
            if err != nil {
                return
            }
            *i = Int(n)
            return
        }
    
        var n int
        err = json.Unmarshal(b, &n)
        if err == nil {
            *i = Int(n)
        }
        return
    }
    
    func main() {
        for _, in := range [...]string{
            `{"Name": "foo", "Balance": 42}`,
            `{"Name": "foo", "Balance": "111"}`,
        } {
            var a account
            err := json.Unmarshal([]byte(in), &a)
            if err != nil {
                fmt.Printf("Error decoding JSON: %v\n", err)
            } else {
                fmt.Printf("Decoded OK: %v\n", a)
            }
        }
    }
    

    Playground link.

  • Handle only a numeric type, and fail anything else with a sensible error:

    package main
    
    import (
        "encoding/json"
        "fmt"
    )
    
    type Int int
    
    type account struct {
        Name    string
        Balance Int
    }
    
    type FormatError struct {
        Want   string
        Got    string
        Offset int64
    }
    
    func (fe *FormatError) Error() string {
        return fmt.Sprintf("Invalid data format at %d: want: %s, got: %s",
            fe.Offset, fe.Want, fe.Got)
    }
    
    func (i *Int) UnmarshalJSON(b []byte) (err error) {
        var n int
        err = json.Unmarshal(b, &n)
        if err == nil {
            *i = Int(n)
            return
        }
        if ute, ok := err.(*json.UnmarshalTypeError); ok {
            err = &FormatError{
                Want:   "number",
                Got:    ute.Value,
                Offset: ute.Offset,
            }
        }
        return
    }
    
    func main() {
        for _, in := range [...]string{
            `{"Name": "foo", "Balance": 42}`,
            `{"Name": "foo", "Balance": "111"}`,
        } {
            var a account
            err := json.Unmarshal([]byte(in), &a)
            if err != nil {
                fmt.Printf("Error decoding JSON: %#v\n", err)
                fmt.Printf("Error decoding JSON: %v\n", err)
            } else {
                fmt.Printf("Decoded OK: %v\n", a)
            }
        }
    }
    

    Playground link.

There is a third possibility: write custom unmarshaler for the whole account type, but it requires more involved code because you'd need to actually iterate over the input JSON data using the methods of the encoding/json.Decoder type.

After reading your

What's the best way to take that incoming request, in Go, and return the error message, "Balance must be a string", for any arbitrary number of integer fields on a request?

more carefully, I admit having a custom parser for the whole type is the only sensible possibility unless you are OK with a 3rd-party package implementing a parser supporting validation via JSON schema (I think I'd look at this first as juju is a quite established product).

kostix
  • 51,517
  • 14
  • 93
  • 176
2

A solution for this could be to use a type assertion by using a map to unmarshal the JSON data into:

type account struct {
    Name    string
    Balance int
}

var str = `{
    "name": "test@example.com", 
    "balance": "3"
}`

func main() {
    var testing = map[string]interface{}{}
    err := json.Unmarshal([]byte(str), &testing)
    if err != nil {
        fmt.Println(err)
    }

    val, ok := testing["balance"]
    if !ok {
        fmt.Println("missing field balance")
        return
    }

    nv, ok := val.(float64)
    if !ok {
        fmt.Println("balance should be a number")
        return
    }

    fmt.Printf("%+v\n", nv)
}

See http://play.golang.org/p/iV7Qa1RrQZ

The type assertion here is done using float64 because it is the default number type supported by Go's JSON decoder.

It should be noted that this use of interface{} is probably not worth the trouble.

The UnmarshalTypeError (https://golang.org/pkg/encoding/json/#UnmarshalFieldError) contains an Offset field that could allow retrieving the contents of the JSON data that triggered the error.

You could for example return a message of the sort:

cannot unmarshal string into Go value of type int near `"balance": "3"`
SirDarius
  • 41,440
  • 8
  • 86
  • 100
0

It would seem that here provides an implementation to work around this issue in Go only.

type account struct {
    Name    string
    Balance int `json:",string"`
}

In my estimation the more correct and sustainable approach is for you to create a client library in something like JavaScript and publish it into the NPM registry for others to use (private repository would work the same way). By providing this library you can tailor the API for the consumers in a meaningful way and prevent errors creeping into your main program.

Community
  • 1
  • 1
Woot4Moo
  • 23,987
  • 16
  • 94
  • 151
  • 1
    This fails in the inverse way when the client sends the value as a number – JimB Feb 09 '16 at 16:56
  • 1
    @JimB read the rest of my statement about doing the right thing from an engineering perspective and supplying a client library – Woot4Moo Feb 09 '16 at 17:02