diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go index 6debb693e..571c5b3c3 100644 --- a/pkg/cli/values/options_test.go +++ b/pkg/cli/values/options_test.go @@ -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)}, }, }, }, diff --git a/pkg/strvals/parser.go b/pkg/strvals/parser.go index be3b1e71b..950c63ab0 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -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 + } +} diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index 73fc3f206..85e60f840 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -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{