feat: add JSON5 configuration support (#5433)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kevin Wan
2026-03-14 21:19:46 +08:00
committed by GitHub
parent 8a2e09dfd1
commit ec802e25a6
6 changed files with 369 additions and 6 deletions

View File

@@ -3,12 +3,63 @@ package encoding
import (
"bytes"
"encoding/json"
"fmt"
"math"
"github.com/pelletier/go-toml/v2"
"github.com/titanous/json5"
"github.com/zeromicro/go-zero/core/lang"
"gopkg.in/yaml.v2"
)
// Json5ToJson converts JSON5 data into its JSON representation.
func Json5ToJson(data []byte) ([]byte, error) {
var val any
if err := json5.Unmarshal(data, &val); err != nil {
return nil, err
}
// Validate that there are no unsupported values like Infinity or NaN
if err := validateJSONCompatible(val); err != nil {
return nil, err
}
return encodeToJSON(val)
}
// validateJSONCompatible checks if the value can be represented in standard JSON.
// JSON5 allows Infinity and NaN, but standard JSON does not support these values.
func validateJSONCompatible(val any) error {
switch v := val.(type) {
case float64:
if math.IsInf(v, 0) {
return fmt.Errorf("JSON5 value Infinity cannot be represented in standard JSON")
}
if math.IsNaN(v) {
return fmt.Errorf("JSON5 value NaN cannot be represented in standard JSON")
}
case []any:
for _, item := range v {
if err := validateJSONCompatible(item); err != nil {
return err
}
}
case map[string]any:
for _, value := range v {
if err := validateJSONCompatible(value); err != nil {
return err
}
}
case map[any]any:
for _, value := range v {
if err := validateJSONCompatible(value); err != nil {
return err
}
}
}
return nil
}
// TomlToJson converts TOML data into its JSON representation.
func TomlToJson(data []byte) ([]byte, error) {
var val any

View File

@@ -1,6 +1,7 @@
package encoding
import (
"math"
"testing"
"github.com/stretchr/testify/assert"
@@ -116,3 +117,142 @@ func TestYamlToJsonSlice(t *testing.T) {
assert.Equal(t, `{"foo":["bar","baz"]}
`, string(b))
}
func TestJson5ToJson(t *testing.T) {
tests := []struct {
name string
input string
expect string
}{
{
name: "standard json",
input: `{"a":"foo","b":1,"c":"${FOO}","d":"abcd!@#$112"}`,
expect: "{\"a\":\"foo\",\"b\":1,\"c\":\"${FOO}\",\"d\":\"abcd!@#$112\"}\n",
},
{
name: "json5 with comments",
input: `{/*comment*/"a":"foo","b":1}`,
expect: "{\"a\":\"foo\",\"b\":1}\n",
},
{
name: "json5 with trailing commas",
input: `{"a":"foo","b":1,}`,
expect: "{\"a\":\"foo\",\"b\":1}\n",
},
{
name: "json5 with unquoted keys",
input: `{a:"foo",b:1}`,
expect: "{\"a\":\"foo\",\"b\":1}\n",
},
{
name: "json5 with single quotes",
input: `{"a":'foo',"b":1}`,
expect: "{\"a\":\"foo\",\"b\":1}\n",
},
{
name: "json5 with line comments",
input: "{\n// This is a comment\n\"a\":\"foo\",\n\"b\":1\n}",
expect: "{\"a\":\"foo\",\"b\":1}\n",
},
{
name: "json5 all features combined",
input: "{\n// comment\na: 'foo', // trailing comma\nb: 1,\n}",
expect: "{\"a\":\"foo\",\"b\":1}\n",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
got, err := Json5ToJson([]byte(test.input))
assert.NoError(t, err)
assert.Equal(t, test.expect, string(got))
})
}
}
func TestJson5ToJsonError(t *testing.T) {
// Invalid JSON5: unquoted string value
_, err := Json5ToJson([]byte("{a: foo}"))
assert.Error(t, err)
}
func TestJson5ToJsonInfinity(t *testing.T) {
// JSON5 allows Infinity but standard JSON does not
_, err := Json5ToJson([]byte(`{value: Infinity}`))
assert.Error(t, err)
assert.Contains(t, err.Error(), "Infinity")
// Negative infinity
_, err = Json5ToJson([]byte(`{value: -Infinity}`))
assert.Error(t, err)
assert.Contains(t, err.Error(), "Infinity")
// Infinity in array
_, err = Json5ToJson([]byte(`{values: [1, Infinity, 3]}`))
assert.Error(t, err)
assert.Contains(t, err.Error(), "Infinity")
}
func TestJson5ToJsonNaN(t *testing.T) {
// JSON5 allows NaN but standard JSON does not
_, err := Json5ToJson([]byte(`{value: NaN}`))
assert.Error(t, err)
assert.Contains(t, err.Error(), "NaN")
// NaN in nested structure
_, err = Json5ToJson([]byte(`{nested: {value: NaN}}`))
assert.Error(t, err)
assert.Contains(t, err.Error(), "NaN")
}
func TestJson5ToJsonSlice(t *testing.T) {
b, err := Json5ToJson([]byte(`{
// comment
foo: [
'bar',
"baz", // trailing comma
],
}`))
assert.NoError(t, err)
assert.Equal(t, `{"foo":["bar","baz"]}
`, string(b))
}
func TestValidateJSONCompatible(t *testing.T) {
// Test float64 types
assert.NoError(t, validateJSONCompatible(float64(1.5)))
assert.Error(t, validateJSONCompatible(math.Inf(1)))
assert.Error(t, validateJSONCompatible(math.Inf(-1)))
assert.Error(t, validateJSONCompatible(math.NaN()))
// Test arrays with invalid values
assert.Error(t, validateJSONCompatible([]any{1, math.Inf(1), 3}))
assert.Error(t, validateJSONCompatible([]any{1, math.NaN(), 3}))
assert.NoError(t, validateJSONCompatible([]any{1, 2, 3}))
// Test map[string]any with invalid values
assert.Error(t, validateJSONCompatible(map[string]any{"value": math.Inf(1)}))
assert.Error(t, validateJSONCompatible(map[string]any{"value": math.NaN()}))
assert.NoError(t, validateJSONCompatible(map[string]any{"value": 1.5}))
// Test map[any]any with invalid values
assert.Error(t, validateJSONCompatible(map[any]any{"value": math.Inf(1)}))
assert.Error(t, validateJSONCompatible(map[any]any{"value": math.NaN()}))
assert.NoError(t, validateJSONCompatible(map[any]any{"value": 1.5}))
// Test nested structures
assert.Error(t, validateJSONCompatible(map[string]any{
"nested": map[string]any{"value": math.Inf(1)},
}))
assert.Error(t, validateJSONCompatible([]any{
map[string]any{"value": math.NaN()},
}))
// Test valid values of various types
assert.NoError(t, validateJSONCompatible("string"))
assert.NoError(t, validateJSONCompatible(42))
assert.NoError(t, validateJSONCompatible(true))
assert.NoError(t, validateJSONCompatible(nil))
}