Skip to content

Silent precision loss for JSON integers > 2^53 in deserialized responses (typed int64 fields and AdditionalProperties) #4164

@charlie-zhang109

Description

@charlie-zhang109

Describe the bug
When a Datadog API response contains an integer above 2^53 (Number.MAX_SAFE_INTEGER), the SDK silently rounds it during JSON deserialization. This affects two categories of fields:

  1. Typed format: int64 fields declared in the OpenAPI spec (e.g. UsageSummaryResponse.fargateTasksCountHwm and the many *_sum / *_agg_sum fields) — the generated model types them as number, and the deserializer uses JSON.parse (packages/datadog-api-client-v{1,2}/models/ObjectSerializer.ts:3390). JSON.parse always produces number (IEEE 754 float64), which can only represent integers exactly up to 2^53.
  2. Untyped fields landing in AdditionalProperties — same JSON.parse path, same float64 coercion, same silent rounding. The failure is silent: the HTTP call succeeds, no parse error is raised, and the value the caller reads from the SDK differs from what the server sent. There is no compile-time, runtime, or test-time signal.

Unlike Go's encoding/json (which has Decoder.UseNumber() to preserve precision in interface{} targets), JavaScript's JSON.parse has no built-in precision-preserving mode. Recovery requires either a third-party library (json-bigint) or server-side stringification of large integers.

Marking as severity/major because the failure is silent. The exposure is broader than the analogous Go bug (DataDog/datadog-api-client-go#4091 (DataDog/datadog-api-client-go#4091)) — in Go, typed *int64 fields preserve precision via strconv.ParseInt and only AdditionalProperties is affected. In TS, both paths share the same JSON.parse boundary, so all typed int64 fields and untyped additionalProperties values are affected.

To Reproduce

import { ObjectSerializer } from "@datadog/datadog-api-client-v1/models/ObjectSerializer";

  // 2^53 + 1 = 9007199254740993 — the smallest integer not exactly representable in float64.
  const body = '{"some_field": 9007199254740993}';
  
  const parsed = ObjectSerializer.parse(body, "application/json");

  console.log(typeof parsed.some_field, parsed.some_field);
  // Output: number 9007199254740992
  //
  // The value 9007199254740993 was silently rounded to 9007199254740992.
  // The caller has no way to recover the original from the resulting `number`.

The same loss occurs for any value above 2^53. For values around 2^56 (e.g. 79940880856921361), the spacing between representable doubles is 16, so the low 4 bits are lost.

In a real SDK call, a typed int64 field exhibits the same behavior:

  const usage = await api.getUsageSummary(...);
  console.log(usage.fargateTasksCountHwmSum);
  // Already rounded if the server returned an integer > 2^53.

Expected behavior
The deserialized value should retain a representation from which the caller can recover the exact original integer. The conventional mechanism in JS is BigInt:

  const parsed = ObjectSerializer.parse(body, "application/json");
  // parsed.some_field is 9007199254740993n (bigint),
  // or string "9007199254740993" with the caller doing BigInt(parsed.some_field).

Environment and Versions (please complete the following information):

  • @datadog/datadog-api-client-v1 and @datadog/datadog-api-client-v2 — reproduced on master.
  • Node.js 20.x.

Additional context
This is reachable in practice today. ingestedEventsBytesAggSum and similar byte-count fields on UsageSummaryResponse reach tens of PB / month at large customers — directly in the lossy band (≥ 2^53). Other
*_agg_sum byte counters are similarly exposed. Affected values silently round to a nearby float64-representable number; for magnitudes around 2^56 (~7 × 10¹⁶) the low 4 bits are unreliable.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions