Skip to content

Commit 9011e83

Browse files
charlie-zhang109claudeci.datadog-api-spec
authored
[Fixed] Preserve integer precision in additionalProperties past 2^53 (#3650)
* Preserve integer precision in additionalProperties past 2^53 Avoid lossy int -> float upconversion in validate_and_convert_types when both int and float are valid target types (e.g. additionalProperties on every ModelNormal subclass). Without this guard, calling float() on any integer above 2^53 silently rounds to the nearest IEEE 754 double, losing the low bits. Fixes #3649. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Apply precision fix to model_utils.j2 template The previous commit patched only the generated src/datadog_api_client/model_utils.py; this commit applies the same guard to the Jinja2 template at .generator/src/generator/templates/model_utils.j2 so the fix survives the next regeneration via ./generate.sh. Verified: rerunning the generator + ruff (pinned to v0.1.4 per .pre-commit-config.yaml) produces no diff against the file from the prior commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * pre-commit fixes --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: ci.datadog-api-spec <packages@datadoghq.com>
1 parent 117979c commit 9011e83

3 files changed

Lines changed: 45 additions & 0 deletions

File tree

.generator/src/generator/templates/model_utils.j2

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,6 +1348,13 @@ def validate_and_convert_types(
13481348
valid_classes_coercible = remove_uncoercible(
13491349
valid_classes, input_value, spec_property_naming, must_convert=False
13501350
)
1351+
# Preserve integer precision past 2^53: skip the (int, float) upconversion
1352+
# when `int` is already a valid target. Without this guard, the loop below
1353+
# would call `float(big_int)` for additionalProperties (whose default
1354+
# `additional_properties_type` contains both float and int) and silently
1355+
# round any integer above 2^53 to the nearest float64 representation.
1356+
if type(input_value) is int and int in valid_classes and float in valid_classes_coercible:
1357+
valid_classes_coercible = [c for c in valid_classes_coercible if c is not float]
13511358
if valid_classes_coercible:
13521359
return attempt_convert_item(
13531360
input_value,

src/datadog_api_client/model_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,13 @@ def validate_and_convert_types(
13641364
valid_classes_coercible = remove_uncoercible(
13651365
valid_classes, input_value, spec_property_naming, must_convert=False
13661366
)
1367+
# Preserve integer precision past 2^53: skip the (int, float) upconversion
1368+
# when `int` is already a valid target. Without this guard, the loop below
1369+
# would call `float(big_int)` for additionalProperties (whose default
1370+
# `additional_properties_type` contains both float and int) and silently
1371+
# round any integer above 2^53 to the nearest float64 representation.
1372+
if type(input_value) is int and int in valid_classes and float in valid_classes_coercible:
1373+
valid_classes_coercible = [c for c in valid_classes_coercible if c is not float]
13671374
if valid_classes_coercible:
13681375
return attempt_convert_item(
13691376
input_value,

tests/test_deserialization.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,34 @@ def test_get_api_test():
308308
path_to_item = ["received_data", "config", "steps"]
309309
converted_value = validate_and_convert_types(value, required_types_mixed, path_to_item, True, True, Configuration())
310310
assert isinstance(converted_value[0], UnparsedObject)
311+
312+
313+
def test_additional_properties_preserve_integer_precision():
314+
"""JSON integers above 2^53 must survive deserialization into a model's
315+
additionalProperties without being silently rounded to float64."""
316+
from datadog_api_client.v1.model.usage_summary_response import UsageSummaryResponse
317+
318+
# 2^53 + 1 — the smallest integer not exactly representable in float64.
319+
edge = 9007199254740993
320+
# A real byte-count value seen in /api/v1/usage/summary at large orgs.
321+
realistic = 56355942906113522
322+
323+
body = json.dumps({"some_unknown_field": edge, "another_unknown_field": realistic})
324+
config = Configuration()
325+
deserialized = validate_and_convert_types(
326+
json.loads(body), (UsageSummaryResponse,), ["received_data"], True, True, config
327+
)
328+
329+
v_edge = deserialized["some_unknown_field"]
330+
v_real = deserialized["another_unknown_field"]
331+
assert type(v_edge) is int and v_edge == edge, f"expected int {edge}, got {type(v_edge).__name__} {v_edge!r}"
332+
assert (
333+
type(v_real) is int and v_real == realistic
334+
), f"expected int {realistic}, got {type(v_real).__name__} {v_real!r}"
335+
336+
337+
def test_schema_declared_float_still_upconverts_int_input():
338+
"""Regression guard: when the schema explicitly declares a field as float,
339+
an integer JSON value must still upconvert to float."""
340+
converted = validate_and_convert_types(3, (float,), ["received_data"], True, True, Configuration())
341+
assert type(converted) is float and converted == 3.0

0 commit comments

Comments
 (0)