go icon indicating copy to clipboard operation
go copied to clipboard

jsoniter's Number is not strict compliance with JSON RFC 7159 standard

Open zhongxinghong opened this issue 3 years ago • 0 comments

version

  • go version go1.16.7 windows/amd64
  • github.com/json-iterator/go v1.1.12
  • github.com/stretchr/testify v1.3.0

code to reproduce this issue

package main

import (
	"bytes"
	"encoding/json"
	ejson "encoding/json"
	"testing"

	jsoniter "github.com/json-iterator/go"
	"github.com/stretchr/testify/require"
)

func ijsonDecode(b []byte, pv interface{}) error {
	ijson := jsoniter.ConfigCompatibleWithStandardLibrary
	d := ijson.NewDecoder(bytes.NewReader(b))
	d.UseNumber()
	return d.Decode(pv)
}

func ejsonDecode(b []byte, pv interface{}) error {
	d := ejson.NewDecoder(bytes.NewReader(b))
	d.UseNumber()
	return d.Decode(pv)
}

func TestJSONNumberDiffIE(t *testing.T) {
	var cases = [][]byte{
		[]byte(`{"a": 01}`),
		[]byte(`{"a": 1.}`),
		[]byte(`{"a": 1.e1}`),
		[]byte(`{"a": -.1}`),
	}
	for _, b := range cases {
		t.Logf("case: %v\n", string(b))
		var iv, ev map[string]interface{}
		ierr := ijsonDecode(b, &iv)
		eerr := ejsonDecode(b, &ev)
		t.Logf("ijson value: %v, error: %v\n", iv, ierr)
		t.Logf("ejson value: %v, error: %v\n", ev, eerr)
		require.NoError(t, ierr) // assert ijson accept it
		require.Error(t, eerr)   // assert ejson reject it
		var inum = iv["a"].(json.Number)
		i64, i64err := inum.Int64()
		f64, f64err := inum.Float64()
		t.Logf("ijson Number: i64: %d, i64err: %v", i64, i64err)
		t.Logf("ijson Number: f64: %f, f64err: %v", f64, f64err)
		// assert ijson's Number meets the golang strconv standard
		require.True(t, i64err == nil || f64err == nil)
	}
}

console output

$ go test -v -count=1 json_test.go -run TestJSONNumberDiffIE
=== RUN   TestJSONNumberDiffIE
    json_test.go:34: case: {"a": 01}
    json_test.go:38: ijson value: map[a:01], error: <nil>
    json_test.go:39: ejson value: map[], error: invalid character '1' after object key:value pair
    json_test.go:45: ijson Number: i64: 1, i64err: <nil>
    json_test.go:46: ijson Number: f64: 1.000000, f64err: <nil>
    json_test.go:34: case: {"a": 1.}
    json_test.go:38: ijson value: map[a:1.], error: <nil>
    json_test.go:39: ejson value: map[], error: invalid character '}' after decimal point in numeric literal
    json_test.go:45: ijson Number: i64: 0, i64err: strconv.ParseInt: parsing "1.": invalid syntax
    json_test.go:46: ijson Number: f64: 1.000000, f64err: <nil>
    json_test.go:34: case: {"a": 1.e1}
    json_test.go:38: ijson value: map[a:1.e1], error: <nil>
    json_test.go:39: ejson value: map[], error: invalid character 'e' after decimal point in numeric literal
    json_test.go:45: ijson Number: i64: 0, i64err: strconv.ParseInt: parsing "1.e1": invalid syntax
    json_test.go:46: ijson Number: f64: 10.000000, f64err: <nil>
    json_test.go:34: case: {"a": -.1}
    json_test.go:38: ijson value: map[a:-.1], error: <nil>
    json_test.go:39: ejson value: map[], error: invalid character '.' in numeric literal
    json_test.go:45: ijson Number: i64: 0, i64err: strconv.ParseInt: parsing "-.1": invalid syntax
    json_test.go:46: ijson Number: f64: -0.100000, f64err: <nil>
--- PASS: TestJSONNumberDiffIE (0.00s)
PASS
ok      command-line-arguments  0.071s

discussion

Golang's encoding/json meets the JSON RFC 7159 standard. Its scanner's FSM will check the number literal's grammar first before parsing it by strconv.ParseInt / strconv.ParseFloat, so some number literals that accepted by strconv will be rejected by encoding/json at the first grammar checking pass. But jsoniter does't check it, instead it treats a string that made up of ['+', '-', '.', 'e', 'E', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] as a json.Number, so some number literals accepted by golang's strconv package but don't meet the JSON RFC 7159 standard will be wrongly accepted by jsoniter.

Recently I try to replace jsoniter with my custom JSON library that modified from encoding/json to speed up our server, I write many unit-tests to ensure all cases that I could think of will have exactly the same decoding result as the encoding/json. But in our production environment I found some DIFF between jsoniter and my JSON library. The jsoniter's README said that it 100% compatibility with standard lib, but it's actually not. From the above test we can confirm that some strange number literals can be wrongly accepted by jsoniter, which will be rejected by encoding/json. This poses a risk to our JSON library migration because some special cases in our online requests which were fine before will cause errors after migration, and it's hard to prevent it to some extent as we don't know if there are any other DIFF between these two JSON libraries.

zhongxinghong avatar Sep 06 '22 15:09 zhongxinghong