Skip to content

encoding/json: should try to convert strings<->numbers without errors if it's possible #22463

Closed
@alexbyk

Description

@alexbyk

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions