From 6d8ee29db81c9228b348244cd5135532aac8ad12 Mon Sep 17 00:00:00 2001 From: simonfaltum Date: Tue, 9 Jun 2026 21:52:57 +0200 Subject: [PATCH 1/2] Fix int64 precision loss in JSON loader JSON numbers were decoded as float64, which corrupts integer values above 2^53 (job, run, and pipeline IDs routinely exceed this). Enable json.Number decoding and parse integer literals as int64; literals with a fraction or exponent stay float64. Co-authored-by: Isaac --- libs/dyn/jsonloader/json.go | 18 +++++++++++ libs/dyn/jsonloader/json_test.go | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/libs/dyn/jsonloader/json.go b/libs/dyn/jsonloader/json.go index 9fcd4e4f2f3..060349e21c8 100644 --- a/libs/dyn/jsonloader/json.go +++ b/libs/dyn/jsonloader/json.go @@ -17,6 +17,11 @@ func LoadJSON(data []byte, source string) (dyn.Value, error) { reader := bytes.NewReader(data) decoder := json.NewDecoder(reader) + // Decode numbers as json.Number instead of float64: a float64 mantissa is + // 53 bits, so int64 values above 2^53 (e.g. job and run IDs) would silently + // lose precision. The token branch in decodeValue picks int64 or float64. + decoder.UseNumber() + // Start decoding from the top-level value value, err := decodeValue(decoder, &offset) if err != nil { @@ -107,6 +112,19 @@ func decodeValue(decoder *json.Decoder, o *Offset) (dyn.Value, error) { } return dyn.NewValue(arr, []dyn.Location{location}), nil } + case json.Number: + // Integer literals become int64 to preserve precision; literals with + // a fraction or exponent (e.g. "2.0", "1e3") stay float64. Integer + // literals that overflow int64 also fall back to float64, matching + // the decoder's behavior before UseNumber was set. + if i64, err := tok.Int64(); err == nil { + return dyn.NewValue(i64, []dyn.Location{location}), nil + } + f64, err := tok.Float64() + if err != nil { + return invalidValueWithLocation(decoder, o), fmt.Errorf("invalid number %q: %w", tok.String(), err) + } + return dyn.NewValue(f64, []dyn.Location{location}), nil default: return dyn.NewValue(tok, []dyn.Location{location}), nil } diff --git a/libs/dyn/jsonloader/json_test.go b/libs/dyn/jsonloader/json_test.go index 995855a111d..200ac405b2b 100644 --- a/libs/dyn/jsonloader/json_test.go +++ b/libs/dyn/jsonloader/json_test.go @@ -6,6 +6,7 @@ import ( "github.com/databricks/cli/libs/dyn/convert" "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const jsonData = ` @@ -111,3 +112,53 @@ func TestJsonValidInline(t *testing.T) { _, err := LoadJSON([]byte(validInline), "path/to/file.json") assert.NoError(t, err) } + +func TestJsonLoaderNumbers(t *testing.T) { + for _, tc := range []struct { + input string + expected any + }{ + {`123`, int64(123)}, + {`-1`, int64(-1)}, + // Above 2^53: would come back as 123456789012345680 if parsed as float64. + {`123456789012345678`, int64(123456789012345678)}, + {`-123456789012345678`, int64(-123456789012345678)}, + {`9223372036854775807`, int64(9223372036854775807)}, + // A fraction or exponent keeps the value a float. + {`2.0`, 2.0}, + {`2.5`, 2.5}, + {`1e3`, 1000.0}, + // Integer literals that overflow int64 fall back to float64. + {`18446744073709551615`, 1.8446744073709552e+19}, + } { + v, err := LoadJSON([]byte(tc.input), "(inline)") + assert.NoError(t, err, tc.input) + assert.Equal(t, tc.expected, v.AsAny(), tc.input) + } +} + +func TestJsonLoaderNumberOutOfRange(t *testing.T) { + _, err := LoadJSON([]byte(`1e400`), "(inline)") + assert.ErrorContains(t, err, "value out of range") +} + +const mixedNumbersData = ` +{ + "job_id": 123456789012345678, + "new_settings": { + "name": "xxx", + "timeout_seconds": 100 + } +} +` + +func TestJsonLoaderMixedNumbersToTyped(t *testing.T) { + v, err := LoadJSON([]byte(mixedNumbersData), "(inline)") + require.NoError(t, err) + + var r jobs.ResetJob + err = convert.ToTyped(&r, v) + require.NoError(t, err) + assert.Equal(t, int64(123456789012345678), r.JobId) + assert.Equal(t, 100, r.NewSettings.TimeoutSeconds) +} From 40d4567b1ca322eaaa3ee887f5963721681344f6 Mon Sep 17 00:00:00 2001 From: simonfaltum Date: Tue, 9 Jun 2026 23:58:35 +0200 Subject: [PATCH 2/2] Remove redundant comments Co-authored-by: Isaac --- libs/dyn/jsonloader/json.go | 9 ++------- libs/dyn/jsonloader/json_test.go | 3 --- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/libs/dyn/jsonloader/json.go b/libs/dyn/jsonloader/json.go index 060349e21c8..3161ef637b1 100644 --- a/libs/dyn/jsonloader/json.go +++ b/libs/dyn/jsonloader/json.go @@ -17,9 +17,7 @@ func LoadJSON(data []byte, source string) (dyn.Value, error) { reader := bytes.NewReader(data) decoder := json.NewDecoder(reader) - // Decode numbers as json.Number instead of float64: a float64 mantissa is - // 53 bits, so int64 values above 2^53 (e.g. job and run IDs) would silently - // lose precision. The token branch in decodeValue picks int64 or float64. + // Use json.Number to avoid losing precision on int64 values above 2^53 (e.g. job and run IDs). decoder.UseNumber() // Start decoding from the top-level value @@ -113,10 +111,7 @@ func decodeValue(decoder *json.Decoder, o *Offset) (dyn.Value, error) { return dyn.NewValue(arr, []dyn.Location{location}), nil } case json.Number: - // Integer literals become int64 to preserve precision; literals with - // a fraction or exponent (e.g. "2.0", "1e3") stay float64. Integer - // literals that overflow int64 also fall back to float64, matching - // the decoder's behavior before UseNumber was set. + // Integers that overflow int64 fall back to float64, matching the decoder's behavior without UseNumber. if i64, err := tok.Int64(); err == nil { return dyn.NewValue(i64, []dyn.Location{location}), nil } diff --git a/libs/dyn/jsonloader/json_test.go b/libs/dyn/jsonloader/json_test.go index 200ac405b2b..1de43c933f1 100644 --- a/libs/dyn/jsonloader/json_test.go +++ b/libs/dyn/jsonloader/json_test.go @@ -120,15 +120,12 @@ func TestJsonLoaderNumbers(t *testing.T) { }{ {`123`, int64(123)}, {`-1`, int64(-1)}, - // Above 2^53: would come back as 123456789012345680 if parsed as float64. {`123456789012345678`, int64(123456789012345678)}, {`-123456789012345678`, int64(-123456789012345678)}, {`9223372036854775807`, int64(9223372036854775807)}, - // A fraction or exponent keeps the value a float. {`2.0`, 2.0}, {`2.5`, 2.5}, {`1e3`, 1000.0}, - // Integer literals that overflow int64 fall back to float64. {`18446744073709551615`, 1.8446744073709552e+19}, } { v, err := LoadJSON([]byte(tc.input), "(inline)")