Javascript’s base type (POD) and JSON actually have only one numeric type: Number. Number is usually represented by the 64-bit floating-point standard in IEEE-754 in mainstream browser implementations (i.e., double-precision floating-point), which represents valid numbers in the range \(-(2^{53} - 1)\) ~ \(2^{53} - 1\) . While 64-bit data types are often used in Go language, e.g., int64/uint64, such values are not safe to use in Javascript.

If you look at the JSON specification document, there is no limit to the number of bits, and any large value can be placed in JSON.

Behavior of json large numbers in Go

Actually tested with the Go language json package, which also does output very large numbers directly

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"encoding/json"
	"fmt"
	"math"
	"math/big"
)

func b2s(b []byte, e error) (string, error) {
	return string(b), e
}

type S struct {
	N *big.Int
}

func main() {
	var i int64 = math.MaxInt64
	fmt.Println(b2s(json.Marshal(i)))

	bi := (&big.Int{}).Mul(big.NewInt(math.MaxInt64), big.NewInt(math.MaxInt64))
	fmt.Println(b2s(json.Marshal(S{N: bi})))
}

Output results.

1
2
9223372036854775807 <nil>
{"N":85070591730234615847396907784232501249} <nil>

These numbers are clearly larger than \(2^{53}\), and Go’s json package still encodes them directly in JSON, which is compliant with the specification.

Safe numbers in Javascript

The values above are not safe numbers in Javascript

1
2
3
4
5
6
7
8
>> Number.isSafeInteger(9223372036854775807)
<< false

>> Number.isSafeInteger(85070591730234615847396907784232501249)
<< false

>> JSON.parse('9223372036854775807')
<< 9223372036854776000

As you can see, the first two sentences output false; the third parse result is not equal to the original number directly.

A number is not safe in Javascript, which means data error, precision loss, and arithmetic error.

Another example is the following official MDN example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const x = Number.MAX_SAFE_INTEGER + 1;
const y = Number.MAX_SAFE_INTEGER + 2;

console.log(Number.MAX_SAFE_INTEGER);
// expected output: 9007199254740991

console.log(x);
// expected output: 9007199254740992

console.log(y);
// expected output: 9007199254740992

console.log(x === y);
// expected output: true

Serializing large numbers with strings in Go

While JSON itself supports numbers of arbitrary size, JSON implementations do not necessarily support them. For example, JSON objects in browsers do not support it. So, in order to use JSON as a data interchange format safely across languages and platforms, data of types like int64 in Go should be encoded using strings.

Obviously, Go must have already taken this into account: all we need to do is to add a string to the JSON TAG of the structure field, and that’s it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
	"encoding/json"
	"fmt"
	"math"
)

type S struct {
	A int64 `json:"a,string"`
}

func main() {
	s1 := S{A: math.MaxInt64}
	b, _ := json.Marshal(s1)
	fmt.Println(string(b))

	s2 := S{}
	json.Unmarshal(b, &s2)
	fmt.Println(s2)
}

Output results.

1
2
{"a":"9223372036854775807"}
{9223372036854775807}

As you can see, the values are encoded as strings and are deserialized correctly.

One unfortunate thing is that this string TAG is only for the following types: string, floating point, integer, boolean.

So N in types like the following will not be stringified and need to do it themselves (not done in this case).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"encoding/json"
	"fmt"
	"math"
	"math/big"
)

type S struct {
	A int64    `json:"a,string"`
	N *big.Int `json:"n,string"`
}

func main() {
	s1 := S{
		A: math.MaxInt64,
		N: (&big.Int{}).Mul(big.NewInt(math.MaxInt64), big.NewInt(math.MaxInt64)),
	}
	b, _ := json.Marshal(s1)
	fmt.Println(string(b))

	s2 := S{}
	json.Unmarshal(b, &s2)
	fmt.Println(s2)
}

Output.

1
2
{"a":"9223372036854775807","n":85070591730234615847396907784232501249}
{9223372036854775807 85070591730234615847396907784232501249}

Deserialize to interface{}

There is another deserialization issue worth mentioning: when using interface{} as the target store to be deserialized, the value type is float64 . This is like the behavior of a browser, and again may result in data loss (float64 is not large enough to hold int64)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"encoding/json"
	"fmt"
	"reflect"
)

func main() {
	var j = `1234567890777888999`
	var i interface{}

	if err := json.Unmarshal([]byte(j), &i); err != nil {
		panic(err)
	}

	fmt.Println(j, reflect.TypeOf(i), i, int64(i.(float64)))
}

Output results.

1
1234567890777888999 float64 1.234567890777889e+18 1234567890777889024

It is obvious that some data is missing (the original number has 19 valid digits, its floating point number has only 16 valid digits), but no error is reported.

One possible way to do this is to have json use its json.Number type to store the values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
)

func main() {
	var j = `1234567890777888999`
	var i interface{}

	d := json.NewDecoder(strings.NewReader(j))
	d.UseNumber()

	if err := d.Decode(&i); err != nil {
		panic(err)
	}

	fmt.Println(reflect.TypeOf(i))

	n := i.(json.Number)
	fmt.Println(j)
	fmt.Println(n.Float64())
	fmt.Println(n.Int64())
	fmt.Println(n.String())
}

Output results.

1
2
3
4
5
json.Number
1234567890777888999
1.234567890777889e+18 <nil>
1234567890777888999 <nil>
1234567890777888999

If this value is too large, Int64() will report an error, Float64() will not (a bit of a weird behavior)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
)

func main() {
	var j = `123456789012345678901234567890`
	var i interface{}

	d := json.NewDecoder(strings.NewReader(j))
	d.UseNumber()

	if err := d.Decode(&i); err != nil {
		panic(err)
	}

	fmt.Println(reflect.TypeOf(i))

	n := i.(json.Number)
	fmt.Println(j)
	fmt.Println(n.Float64())
	fmt.Println(n.Int64())
	fmt.Println(n.String())
}

Output results.

1
2
3
4
5
json.Number
123456789012345678901234567890
1.2345678901234568e+29 <nil>
9223372036854775807 strconv.ParseInt: parsing "123456789012345678901234567890": value out of range
123456789012345678901234567890

Or, can we simply determine Float64() == String() ?

Protocol Buffers’ handling of int64

As seen in golang/protobuf, Protocol Buffers treats all int64/uint64 serialized as strings by default.

1
2
3
case int64, uint64:
	w.write(fmt.Sprintf(`"%d"`, v.Interface()))
	return nil

Performance issues?

Some people may think that transferring values as strings is not a big performance impact?

I don’t think so, because we’re talking about JSON, and what’s the difference between a string and a value in JSON? It’s just that strings have two more quotes. Using a string to represent a numeric value takes up more memory, but at the transfer level, it’s just two more bytes. Theoretically, this has less impact on performance.