Description
Trying to combine api/JSON/databases in Go is a pain. The problem is it's very hard with JS applications/frameworks to be sure about the correctness of JSON output. Most probably everyone ends up with something like this:
// let product= {id: "1", price: "100"}
product.id = +product.id;
product.price = +product.price;
// .. send to API
Go could help a lot in this cases and the code above could be avoided, because Go has all the information needed to correctly convert, for example, this JSON {"id": "1", "price": "100"}
into this structure
type Product struct {
ID int64 `json:"id"`
Price float64 `json:"price"`
}
... but instead of that it panics
Even more, there is a json.Number
type which partially solves the problem, but still you should do a lot of checks and conversions (like product.id.Float64()
) because it's not a basic type and also database/sql
doesn't support it.
The only way I found to deal with this stuff is to create a new type, based on int64, and implement json.Unmarshaler
interface. But it's still a lot of typing and error prone
The full example about what time-saving encoding/json could do out of the box (maybe with some flags to not break an existing code)
package main
import (
"encoding/json"
"fmt"
)
type Product struct {
ID int64 `json:"id"`
Price float64 `json:"price"`
}
func parseJSON(p *Product, str string) error {
err := json.Unmarshal([]byte(str), p)
if err != nil {
return err
}
return nil
}
func ExampleJson() {
strs := []string{
// works
`{"id": 1, "price": 100}`,
`{"id": 1, "price": 100.0}`,
// this should work too because Go has enough information
// to convert these strings into int64/float64, but it doesn't :(
`{"id": 1, "price": "100"}`,
`{"id": 1, "price": "100.00"}`,
`{"id": "1", "price": 100}`,
}
for _, str := range strs {
p := Product{}
err := parseJSON(&p, str)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v, discount: %v\n", p, p.Price*0.1)
}
// Output:
// {ID:1 Price:100}, discount: 10
// {ID:1 Price:100}, discount: 10
// {ID:1 Price:100}, discount: 10
// {ID:1 Price:100}, discount: 10
// {ID:1 Price:100}, discount: 10
}
In my opinion, err
shouldn't be nil
only in case when Product.id
can't be converted to int64
, Product.price
to float64
and so on. The client code shouldn't be aware of what internal types are we using
Also the same should be true about this case:
var id string
err := json.Unmarshal([]byte(`123`), &id)
if err != nil {
panic(err)
}
fmt.Println(id)
Go has enough information to assume that we want to convert JSON number 123
to Go's string, because it's the only reasonable case and JS client code shouldn't be bothered what's our id
's internal type. But instead it panics
If encoding/json could behave like this, it would make working with JSON in Go much more pleasant and concise
UPD: Actually it's not a cool feature, It's more like an obvious expectation. For example, database/sql works as expected with this
var idStr string
db.QueryRow("select id from categories limit 1").Scan(&idStr)
var idInt int64
db.QueryRow("select id from categories limit 1").Scan(&idInt)
fmt.Printf("%+v\n", idStr)
fmt.Printf("%+v\n", idInt)
idStr
will be "1"
and idInt
will be 1
(for example). So json
should follow the same logic
Writing custom types with custom MarshalJSON/UnmarshalJSON isn't trivial and working with custom types isn't a pleasure at all. Also casting js
code (id = +id
) or writing strconv.Atoi()
for every json input is annoying.
As a +1 for this proposal, there is a json.Number
type and a string
tag. I assume they were created exactly for such kind of cases. But they don't work and look like a hack. With the behavior, explained above, there will be no need of them. At least when dealing with JS api clients
As a +2, this behavior can save a lot of time for backend and frontend developers. Golang is a typed language, why not to use it at full strength?