Skip to main content

Number handling and precision

JSON has exactly one number type — an arbitrary-precision decimal, syntactically. C++ has several, each with a bounded range and its own precision characteristics. The mapping between them is where this library's silent data loss lives, and it's worth understanding before it surprises you in production.

Three internal number types

value_tDefault C++ typeChosen when
number_integer_tint64_tThe literal has no ./e and fits in a signed 64-bit range
number_unsigned_tuint64_tThe literal has no ./e, is non-negative, and exceeds the signed 64-bit range
number_float_tdoubleThe literal has a . or exponent, or an integer literal that doesn't fit either integer type

Parse-time type selection

for (auto text : {"1", "-1", "1.0", "1e2"}) {
json j = json::parse(text);
std::cout << text << " -> " << j.type_name() << "\n";
}
// 1 -> number
// -1 -> number
// 1.0 -> number
// 1e2 -> number

type_name() reports "number" for all three internal kinds — the distinction only shows up via is_number_integer() / is_number_unsigned() / is_number_float(). 1 and 1.0 end up in different internal storage (number_integer_t vs number_float_t) despite json::parse("1") == json::parse("1.0") evaluating to true, because equality compares numeric value, not storage kind.

Integers that don't fit

Integers beyond int64_t/uint64_t silently become double

A JSON integer literal larger than UINT64_MAX (about 1.8 × 10^19) has nowhere left to go in the default basic_json — the parser falls back to storing it as number_float_t, silently losing precision in the process:

json j = json::parse("18446744073709551616"); // 2^64, one past uint64_t's max
// stored as a double — the exact integer value is now unrecoverable

There's no warning and no exception; j.dump() will print a number, just not necessarily the one that was in the input. If a field might legitimately carry integers beyond 64 bits (large IDs from some external system, arbitrary-precision computation results), don't trust round-tripping through json — see "Big numbers" below.

Float round-tripping

Floating-point values dump using a shortest-round-trip algorithm, so json::parse(j.dump()) == j holds for ordinary finite doubles. NaN and Inf are the exception:

NaN and Inf dump as null

JSON's grammar has no representation for NaN or infinity — there's no token for it. dump() handles both by emitting null instead of throwing, which means a document containing NaN or Inf does not round-trip: parsing the dumped output back gives you null, not the original value. If a computed value might be NaN/Inf and that distinction matters downstream, check for it and encode it explicitly (a string sentinel, a separate boolean flag) before it reaches dump().

Customising the number types

basic_json's template parameter list includes NumberIntegerType, NumberUnsignedType, and NumberFloatType — instantiating your own alias lets you widen (or narrow) any of the three independently, for example using long double instead of double where the platform supports meaningfully more precision. See Custom allocators and JSON types for the full parameter list and a worked custom instantiation.

Big numbers

For values that need more range or precision than any of the three number kinds can hold — 128-bit IDs, arbitrary-precision decimals, currency amounts where floating-point rounding is unacceptable — the practical answer isn't a wider number type, it's not using JSON's number type at all: store the value as a JSON string and convert on the way in and out with a custom adl_serializer. See adl_serializer and template types for the pattern.

See also