fix(strvals): preserve integer precision in --set-json

--set-json decoded JSON numbers into an any via encoding/json, so every
number became a float64 and integers above 2^53 lost precision:
9007199254740993 became 9007199254740992, and larger values rendered in
scientific notation. The plain --set path parses integers as int64
(typedVal), so the two flags disagreed on the same input.

Decode with UseNumber and convert each integral json.Number to int64
(fractional ones stay float64), recursing through objects and arrays.

Signed-off-by: Sueun Cho <sueun.dev@gmail.com>
pull/32520/head
Sueun Cho 1 month ago
parent e8a24895d1
commit 7bf81bd069

@ -294,7 +294,7 @@ func TestMergeValuesCLI(t *testing.T) {
},
expected: map[string]any{
"foo": map[string]any{
"bar": []any{1.0, 2.0, 3.0},
"bar": []any{int64(1), int64(2), int64(3)},
},
},
},

@ -224,10 +224,11 @@ func (t *parser) key(data map[string]any, nestedNameLevel int) (reterr error) {
// discard in t.sc the chars of the decoded json value (the number of those characters is returned by InputOffset).
var jsonval any
dec := json.NewDecoder(strings.NewReader(t.sc.String()))
dec.UseNumber()
if err := dec.Decode(&jsonval); err != nil {
return err
}
set(data, string(k), jsonval)
set(data, string(k), convertJSONNumbers(jsonval))
if _, err = io.CopyN(io.Discard, t.sc, dec.InputOffset()); err != nil {
return err
}
@ -356,10 +357,11 @@ func (t *parser) listItem(list []any, i, nestedNameLevel int) ([]any, error) {
// discard in t.sc the chars of the decoded json value (the number of those characters is returned by InputOffset).
var jsonval any
dec := json.NewDecoder(strings.NewReader(t.sc.String()))
dec.UseNumber()
if err := dec.Decode(&jsonval); err != nil {
return list, err
}
if list, err = setIndex(list, i, jsonval); err != nil {
if list, err = setIndex(list, i, convertJSONNumbers(jsonval)); err != nil {
return list, err
}
if _, err = io.CopyN(io.Discard, t.sc, dec.InputOffset()); err != nil {
@ -555,3 +557,33 @@ func typedVal(v []rune, st bool) any {
return val
}
// convertJSONNumbers walks a value decoded by encoding/json with UseNumber
// enabled and turns every json.Number into an int64 when it is integral, or a
// float64 otherwise. Decoding into an any without UseNumber makes every JSON
// number a float64, which silently loses precision for integers beyond 2^53
// (for example 9007199254740993 becomes 9007199254740992). This keeps the
// --set-json path consistent with the plain --set path, whose typedVal parses
// integers as int64.
func convertJSONNumbers(v any) any {
switch val := v.(type) {
case json.Number:
if iv, err := val.Int64(); err == nil {
return iv
}
fv, _ := val.Float64()
return fv
case map[string]any:
for k, e := range val {
val[k] = convertJSONNumbers(e)
}
return val
case []any:
for i, e := range val {
val[i] = convertJSONNumbers(e)
}
return val
default:
return v
}
}

@ -642,6 +642,51 @@ func TestParseJSON(t *testing.T) {
}
}
func TestParseJSONNumbers(t *testing.T) {
// A JSON integer must round-trip exactly, including values above 2^53
// where a float64 would lose precision. This mirrors the plain --set
// path, which parses integers as int64.
tests := []struct {
name string
input string
expect map[string]any
}{
{
name: "integer beyond 2^53 keeps its exact value",
input: "id=9007199254740993",
expect: map[string]any{"id": int64(9007199254740993)},
},
{
name: "small integer decodes as int64",
input: "n=3",
expect: map[string]any{"n": int64(3)},
},
{
name: "negative large integer keeps its exact value",
input: "n=-9223372036854775807",
expect: map[string]any{"n": int64(-9223372036854775807)},
},
{
name: "fractional value stays a float",
input: "ratio=1.5",
expect: map[string]any{"ratio": float64(1.5)},
},
{
name: "integers nested in objects and arrays keep their exact value",
input: `obj={"big":9007199254740993,"list":[9007199254740995]}`,
expect: map[string]any{"obj": map[string]any{"big": int64(9007199254740993), "list": []any{int64(9007199254740995)}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := map[string]any{}
err := ParseJSON(tt.input, got)
require.NoError(t, err)
assert.Equal(t, tt.expect, got)
})
}
}
func TestParseFile(t *testing.T) {
input := "name1=path1"
expect := map[string]any{

Loading…
Cancel
Save