diff --git a/libs/dyn/jsonloader/json.go b/libs/dyn/jsonloader/json.go index 9fcd4e4f2f3..3161ef637b1 100644 --- a/libs/dyn/jsonloader/json.go +++ b/libs/dyn/jsonloader/json.go @@ -17,6 +17,9 @@ func LoadJSON(data []byte, source string) (dyn.Value, error) { reader := bytes.NewReader(data) decoder := json.NewDecoder(reader) + // 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 value, err := decodeValue(decoder, &offset) if err != nil { @@ -107,6 +110,16 @@ func decodeValue(decoder *json.Decoder, o *Offset) (dyn.Value, error) { } return dyn.NewValue(arr, []dyn.Location{location}), nil } + case json.Number: + // 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 + } + 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..1de43c933f1 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,50 @@ 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)}, + {`123456789012345678`, int64(123456789012345678)}, + {`-123456789012345678`, int64(-123456789012345678)}, + {`9223372036854775807`, int64(9223372036854775807)}, + {`2.0`, 2.0}, + {`2.5`, 2.5}, + {`1e3`, 1000.0}, + {`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) +}