Data Formats (JSON, YAML, TOML, XML, CSV) Module 2 – JSON in depth
Numbers: precision, big integers and decimals
Why JSON numbers change value when parsed, how to keep 64-bit ids and money exact in JavaScript and Python, and how JSON.rawJSON and reviver source text help.
What you will learn
- Predict when a JSON number changes value after parsing in JavaScript and Python
- Parse and emit 64-bit identifiers and money amounts without losing digits
- Use JSON.rawJSON and the reviver's source text where supported, with a tested fallback
- Choose a representation for amounts from integer minor units, decimal strings and decimal types
Before you start
What you’ll learn · Before you start
What you will learn
- Predict when a JSON number changes value after parsing in JavaScript and Python
- Parse and emit 64-bit identifiers and money amounts without losing digits
- Use JSON.rawJSON and the reviver's source text where supported, with a tested fallback
- Choose a representation for amounts from integer minor units, decimal strings and decimal types
Before you start
On this page
JSON.parse("9007199254740993") returns 9007199254740992, and the 20-digit order id 12345678901234567890 comes back
as 12345678901234567000, both without an error. JSON numbers are decimal text with no size limit, and the reader decides
what they become, which is why the same text can be exact in Python and quietly different in JavaScript.
You will predict which numbers change on the way through each parser, keep 64-bit ids and money amounts exact, use
ECMAScript 2026’s JSON.rawJSON and the reviver’s source text with a fallback for engines that lack them, and choose a
representation for amounts that every reader handles.
A JSON number is decimal text, and JavaScript reads it as binary64
The grammar of RFC 8259 puts no limit on the digits of a number. Its section 6 warns instead that software which
implements IEEE 754 binary64: The IEEE 754 64-bit floating-point format, also called double precision, with 53 bits of precision. Every JavaScript number and every Python float is a binary64 value., double precision, is common, and that good interoperability
comes from expecting no more precision or range than binary64 provides. Every JavaScript number is a binary64 value.
Here are seven JSON numbers through JSON.parse:
// The same JSON numbers through JSON.parse: every one becomes a binary64 Number.
const texts = ['9007199254740991', '9007199254740992', '9007199254740993', '12345678901234567890', '1.10', '1e400', '-0'];
for (const text of texts) {
const n = JSON.parse(text);
const shown = Object.is(n, -0) ? '-0' : String(n);
console.log(`${text.padEnd(22)} -> ${shown.padEnd(22)} safe integer: ${Number.isSafeInteger(n)}`);
}
console.log('0.1 + 0.2 =', JSON.parse('0.1') + JSON.parse('0.2'));
console.log('MAX_SAFE_INTEGER =', Number.MAX_SAFE_INTEGER); Output
9007199254740991 -> 9007199254740991 safe integer: true 9007199254740992 -> 9007199254740992 safe integer: false 9007199254740993 -> 9007199254740992 safe integer: false 12345678901234567890 -> 12345678901234567000 safe integer: false 1.10 -> 1.1 safe integer: false 1e400 -> Infinity safe integer: false -0 -> -0 safe integer: true 0.1 + 0.2 = 0.30000000000000004 MAX_SAFE_INTEGER = 9007199254740991
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node precision_probe.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Go two past the limit
Press Edit, add '9007199254740995' to texts and run it again. It comes back as 9007199254740996: halfway
between two binary64 neighbours, the tie goes to the one whose last bit is 0.
Up to 9007199254740991, 2^53 − 1, every integer survived, and Number.isSafeInteger says so: that is
Number.MAX_SAFE_INTEGER, the largest safe integer: An integer that binary64 holds exactly and shares with no other integer: from -(2^53 - 1) to 2^53 - 1, which is 9007199254740991. Number.isSafeInteger tests for it.. One step further, 9007199254740993
became 9007199254740992, and the 20-digit id came back ending in 000 instead of 890. 1.10 became 1.1, which is the same number,
but 1e400 became Infinity, because it is beyond the largest binary64 value, and -0 stayed negative zero. Step
through what happened to the id:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Twenty digits arrive. The text is 12345678901234567890, a valid JSON number. JSON itself sets no limit on its digits, so the text means exactly that integer.
- Binary64 cannot hold it. A binary64 number has 53 bits of precision. Near 1.2 × 10^19 neighbouring values lie 2048 apart, so most integers there have no binary64 value of their own.
- The nearest one is taken. JSON.parse takes the nearest binary64 value, 12345678901234567168, which is 722 below the integer the text meant. No error is raised.
- Printed with the fewest digits. Printing chooses the shortest digits that read back as the same binary64 value: 12345678901234567000. The id now ends in 000 instead of 890.
- One above 2 to the 53rd. At 2^53 the gap between neighbours is 2. 9007199254740993 lies exactly halfway between 9007199254740992 and 9007199254740994, and the tie goes to 9007199254740992.
- A different id, the same Number. JSON.parse returned 9007199254740992 for both 9007199254740992 and 9007199254740993: two different ids became one Number, so lookups by id can return the wrong order.
Two different ids, 9007199254740992 and 9007199254740993, became one Number. The ECMAScript specification gives
exactly that pair as its example: integers above Number.MAX_SAFE_INTEGER share their value with at least one other
integer. RFC 7493, the I-JSON profile, draws the practical conclusion: senders cannot expect receivers to handle
integers whose absolute value is greater than 9007199254740991 exactly, and it recommends sending such numbers, such
as 64-bit ids, as strings.
Key idea
A JSON reader that uses binary64 keeps integers exact only up to 2^53 − 1, 9007199254740991; above that, and for most decimal fractions, it keeps the nearest binary64 value, silently.
Checkpoint
Check: Binary64 and the safe integer limit
Python keeps integers exact, and reads decimals exactly when asked
Python’s json module reads a number without a fraction or exponent as an int, and Python integers have no fixed
size, so every id stays exact. Numbers with a fraction or an exponent become float, the same binary64 as in
JavaScript:
"""The same JSON numbers through json.loads: integers stay exact, the rest become binary64 floats."""
import json
import math
texts = ["9007199254740991", "9007199254740992", "9007199254740993", "12345678901234567890", "1.10", "1e400", "-0"]
for text in texts:
value = json.loads(text)
print(f"{text:<22} -> {value!r:<22} {type(value).__name__}")
print("0.1 + 0.2 =", json.loads("0.1") + json.loads("0.2"))
# What a binary64 reader such as JSON.parse keeps of the 20-digit id: the nearest float, and its neighbours.
nearest = float(12345678901234567890)
print("nearest binary64 to 12345678901234567890:", int(nearest), "gap to the next:", math.ulp(nearest))
print("gap between floats at 2**53:", math.ulp(2.0**53)) Output
9007199254740991 -> 9007199254740991 int 9007199254740992 -> 9007199254740992 int 9007199254740993 -> 9007199254740993 int 12345678901234567890 -> 12345678901234567890 int 1.10 -> 1.1 float 1e400 -> inf float -0 -> 0 int 0.1 + 0.2 = 0.30000000000000004 nearest binary64 to 12345678901234567890: 12345678901234567168 gap to the next: 2048.0 gap between floats at 2**53: 2.0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 precision_probe.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
All four integers came back exact, the 20-digit id included, but 0.1 + 0.2 is 0.30000000000000004, as it is in
JavaScript, because both are binary64 sums. The last two lines show why: the binary64 value nearest to the id is
12345678901234567168, and neighbouring values there are 2048 apart. -0 became the integer 0: an int has no
negative zero.
For decimal amounts, Python can skip floats altogether: json.loads(text, parse_float=Decimal) hands every number
with a fraction to decimal.Decimal as its text. This program reads the same three amounts three ways:
"""Three ways to read the amounts of an order: as floats, as Decimal, and as whole paise."""
import json
from decimal import Decimal
text = '{"amounts": [0.10, 0.20, 0.30], "amounts_paise": [10, 20, 30]}'
a, b, c = json.loads(text)["amounts"]
print("floats, a + b + c: ", a + b + c)
print("floats, sum(): ", sum([a, b, c]), "(compensated, but still binary floats)")
decimals = json.loads(text, parse_float=Decimal)["amounts"]
print("Decimal, sum(): ", sum(decimals))
paise = sum(json.loads(text)["amounts_paise"])
print("whole paise, sum(): ", paise, "->", f"{paise // 100}.{paise % 100:02d}")
print("1.15 * 100 as floats:", 1.15 * 100, "| as Decimal:", Decimal("1.15") * 100)
try:
json.loads("1" * 4301)
except ValueError as e:
print("an integer of 4,301 digits:", str(e).split(";")[0]) Output
floats, a + b + c: 0.6000000000000001 floats, sum(): 0.6 (compensated, but still binary floats) Decimal, sum(): 0.60 whole paise, sum(): 60 -> 0.60 1.15 * 100 as floats: 114.99999999999999 | as Decimal: 115.00 an integer of 4,301 digits: Exceeds the limit (4300 digits) for integer string conversion: value has 4301 digits
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 decimal_amounts.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Adding the floats with + gave 0.6000000000000001. sum() gave 0.6, because since Python 3.12 it uses
Neumaier summation, a compensated sum, for floats; its result is still a binary64 value, only a closer one. The
Decimal sum kept the two decimal places, 0.60, and so did the whole paise. The last line is a limit of a different
kind: Python refuses to turn a string of more than 4,300 digits into an int, by default, to stop slow conversions
of huge numbers, so json.loads of a 4,301-digit integer raises ValueError.
Trusting a float that prints correctly
Wrong: amount = float("1.15") and then int(amount * 100), which gives 114, because 1.15 * 100 is
114.99999999999999, as the run printed. Right: parse with parse_float=Decimal, or receive whole paise, and keep
binary floats away from money.
Checkpoint
Check: Python's int, float and Decimal
JavaScript keeps big integers exact with the reviver’s source text
ECMAScript 2026 includes the proposal “JSON.parse source text access”. It gives a reviver a third argument, a context
object whose source is the original text of each primitive value, and it adds JSON.rawJSON, which puts a piece of
JSON text into JSON.stringify’s output unchanged. Together they read and write 64-bit ids exactly. Because older
engines lack them, test for them first and decide what happens without them:
// Read and write 64-bit order ids exactly: the reviver's source text and JSON.rawJSON where the engine has them
// (ECMAScript 2026), and a refusal instead of a silently changed id where it does not.
const text = '{"orders": [{"id": 12345678901234567890, "qty": 2}, {"id": 9007199254740993, "qty": 1}]}';
const hasSourceText = typeof JSON.rawJSON === 'function';
console.log('JSON.parse source text access:', hasSourceText ? 'supported' : 'not supported');
function exactIds(key, value, context) {
if (key !== 'id' || typeof value !== 'number') return value;
if (hasSourceText) return BigInt(context.source); // the digits as written, before rounding
if (!Number.isSafeInteger(value)) throw new RangeError(`id ${value} is not exact: send it as a string`);
return BigInt(value);
}
try {
const data = JSON.parse(text, exactIds);
for (const o of data.orders) console.log('id', o.id.toString(), 'qty', o.qty);
// Writing back: JSON.rawJSON puts the digits into the text unchanged.
const out = JSON.stringify(data, (key, value) => (typeof value === 'bigint' ? JSON.rawJSON(value.toString()) : value));
console.log(out);
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
console.log('without a reviver:', JSON.stringify(JSON.parse(text).orders.map((o) => o.id))); Output
JSON.parse source text access: supported
id 12345678901234567890 qty 2
id 9007199254740993 qty 1
{"orders":[{"id":12345678901234567890,"qty":2},{"id":9007199254740993,"qty":1}]}
without a reviver: [12345678901234567000,9007199254740992]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node exact_ids.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
The Run button runs JavaScript in QuickJS, which has neither the reviver’s context nor JSON.rawJSON, so the
program takes its other path there: it refuses the first id, which a Number cannot hold, instead of returning a
wrong one. Node.js 24.21.0, which recorded the output above, supports both.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
JSON.parse source text access: not supported RangeError: id 12345678901234567000 is not exact: send it as a string without a reviver: [12345678901234567000,9007199254740992]
With the feature, the reviver turned the digits of each id into a BigInt, BigInt(context.source), before any
rounding, and the replacer wrote them back with JSON.rawJSON(value.toString()), so the output text holds the same
digits as the input. The last line is the same text through JSON.parse with no reviver: both ids changed.
The fallback matters as much as the feature. A program that silently uses the rounded Number when the engine lacks
source text access gives wrong ids on exactly the browsers it was not tested in. Refusing with a clear error, or
asking the sender for strings, is the safe choice.
Strings for ids, always
If you control the API, send 64-bit ids as strings, "id": "12345678901234567890", as I-JSON recommends. Every
language and engine reads a string exactly, and an id is a label, not a quantity you add up.
Checkpoint
Check: Exact big integers in JavaScript
Money travels as whole minor units or decimal strings
Amounts need exact sums, and binary64 cannot hold most decimal fractions, so a contract for money chooses one of three representations, all of which every JSON reader handles:
| Representation | In the JSON | Exact in | Cost |
|---|---|---|---|
| Whole minor unit: The smallest unit of a currency that amounts are counted in, such as the cent or the paisa. Storing amounts as whole minor units keeps every sum exact. | "amount_paise": 37900 |
Every language, up to 2^53 − 1 minor units in JavaScript | Every reader divides by 100 for display, and must know the currency’s minor unit |
| Decimal string | "amount": "379.00" |
Every language that parses it with a decimal type or as paise | Each reader needs a parser for the string; numbers are not numbers in the JSON |
| JSON number, read as a decimal type | "amount": 379.00 |
Python with parse_float=Decimal; JavaScript only with source text access |
Every reader must be configured, and one careless reader rounds |
Whole minor units are the simplest contract: integers are exact everywhere, sums are integer sums, and the decimal
point appears only when an amount is shown. Decimal strings suit amounts with more decimal places, such as unit
prices, and amounts that people read in the raw JSON. A JSON number with a fraction is the riskiest, because it is
exact only for readers that were told to keep it so. Whichever you choose, name the currency in the same object, with
its three-letter code, such as "currency": "INR".
Rupees in paise
₹1,23,456.50, one lakh twenty-three thousand four hundred fifty-six rupees and fifty paise, is the whole number
12345650 in paise. The grouping of lakhs and crores is a matter of display: the JSON carries 12345650 and the
currency, and each page formats it for its reader.
Checkpoint
Check: Representations for money
TOML, CSV and spreadsheets set number limits of their own
JSON is not the only format whose numbers outrun their readers. TOML recommends that parsers accept and handle at least 64-bit signed integers, from −2^63 to 2^63 − 1, losslessly, and requires an error for an integer that cannot be represented losslessly. CSV has no numbers at all, only text, so the limit is whatever the reader converts to:
"""The same big numbers in TOML and CSV, read with Python's standard library."""
import csv
import io
import tomllib
doc = tomllib.loads("max64 = 9223372036854775807\nbeyond = 9223372036854775808\nratio = 0.1")
for key, value in doc.items():
print(f"TOML {key:<7} {value!r:<22} {type(value).__name__}")
rows = list(csv.DictReader(io.StringIO("id,amount\n12345678901234567890,379.10\n")))
row = rows[0]
print("CSV id ", repr(row["id"]), "-> float():", float(row["id"]), "-> int():", int(row["id"]))
print("CSV amount ", repr(row["amount"]), "-> float():", float(row["amount"])) Output
TOML max64 9223372036854775807 int TOML beyond 9223372036854775808 int TOML ratio 0.1 float CSV id '12345678901234567890' -> float(): 1.2345678901234567e+19 -> int(): 12345678901234567890 CSV amount '379.10' -> float(): 379.1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 other_formats.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
tomllib returned both integers as exact Python ints, even 2^63, one past the 64-bit range, because a Python int
can hold it losslessly. A JavaScript TOML parser that keeps integers as Number cannot hold 9223372036854775807
exactly, so by TOML’s rule it must refuse it, unless it uses BigInt. From CSV, the id came back as a string, which
int() keeps exact and float() rounds to 1.2345678901234567e+19. Spreadsheets add one more limit: Excel keeps 15
significant digits, so a 16-digit or 20-digit id typed as a number loses digits the moment the file is opened.
Checkpoint
Check: Number limits in TOML, CSV and spreadsheets
Practise: amounts as exact paise
The exercise implements the representation this lesson recommends, in Python or JavaScript: a parser that turns a decimal string into whole paise without any float, refusing what is not an amount, and its inverse for display:
Exercise · Easy · Python, JavaScript
Convert decimal amounts to whole paise exactly
An API sends amounts as decimal strings, the way RFC 7493 advises for values that binary64 cannot hold exactly: "19.99", "-5.50", "100". Write two functions, in Python (paise.py) or JavaScript (paise.mjs):
to_paise(amount)(toPaisein JavaScript) returns the amount as a whole number of paise, exactly:"19.99"is 1999,"0.1"is 10,"100"is 10000 and"-5.50"is -550. It raises an error (ValueErrorin Python, anyErrorin JavaScript) for anything else: more than two decimal places, an exponent, a plus sign, spaces, an empty string or a lone full stop.from_paise(paise)(fromPaise) writes paise back as a string with exactly two decimal places: 1999 is"19.99", 5 is"0.05"and -550 is"-5.50".
Do not go through a float: parseFloat("19.99") * 100 is not 1999.
Python · Starter code · paise.py
import re
def to_paise(amount):
"""Whole paise in a decimal string with at most two decimal places, such as "19.99"."""
return int(float(amount) * 100)
def from_paise(paise):
"""Paise written as a decimal string with exactly two decimal places."""
return f"{paise / 100:.2f}" The sample tests · test_paise.py
from paise import from_paise, to_paise
def refused(amount):
"""Whether to_paise(amount) raises ValueError."""
try:
to_paise(amount)
except ValueError:
return True
return False
def test_exact_amounts():
"""converts amounts that a float would get wrong"""
assert to_paise("19.99") == 1999
assert to_paise("1.15") == 115
assert to_paise("0.1") == 10
assert to_paise("100") == 10000
def test_negative_and_large():
"""keeps the sign, and every digit of a large amount"""
assert to_paise("-5.50") == -550
assert to_paise("-0.05") == -5
assert to_paise("12345678901234.56") == 1234567890123456
def test_refuses_bad_text():
"""refuses three decimals, exponents, signs, spaces and empty text"""
for amount in ["1.005", "1e3", "+1.00", " 1.00", "1.00 ", "", ".", "1.", "-", "1,00"]:
assert refused(amount), amount
def test_from_paise():
"""writes exactly two decimal places, with the sign"""
assert from_paise(1999) == "19.99"
assert from_paise(5) == "0.05"
assert from_paise(-550) == "-5.50"
assert from_paise(1234567890123456) == "12345678901234.56" JavaScript · Starter code · paise.mjs
/** Whole paise in a decimal string with at most two decimal places, such as "19.99". */
export function toPaise(amount) {
return Math.trunc(parseFloat(amount) * 100);
}
/** Paise written as a decimal string with exactly two decimal places. */
export function fromPaise(paise) {
return (paise / 100).toFixed(2);
} The sample tests · paise.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { fromPaise, toPaise } from './paise.mjs';
const refused = (amount) => {
try {
toPaise(amount);
} catch {
return true;
}
return false;
};
test('converts amounts that a float would get wrong', () => {
assert.equal(toPaise('19.99'), 1999);
assert.equal(toPaise('1.15'), 115);
assert.equal(toPaise('0.1'), 10);
assert.equal(toPaise('100'), 10000);
});
test('keeps the sign, and every digit of a large amount', () => {
assert.equal(toPaise('-5.50'), -550);
assert.equal(toPaise('-0.05'), -5);
assert.equal(toPaise('12345678901234.56'), 1234567890123456);
});
test('refuses three decimals, exponents, signs, spaces and empty text', () => {
for (const amount of ['1.005', '1e3', '+1.00', ' 1.00', '1.00 ', '', '.', '1.', '-', '1,00']) assert.ok(refused(amount), JSON.stringify(amount));
});
test('writes exactly two decimal places, with the sign', () => {
assert.equal(fromPaise(1999), '19.99');
assert.equal(fromPaise(5), '0.05');
assert.equal(fromPaise(-550), '-5.50');
assert.equal(fromPaise(1234567890123456), '12345678901234.56');
}); A hint
Check the whole text with one regular expression first, an optional minus, digits, and an optional full stop with one or two digits: ^(-?)([0-9]+)(?:\.([0-9]{1,2}))?$. Then build the result from the parts with integer arithmetic: rupees times 100, plus the fraction padded to two digits, with the sign applied at the end.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then test yourself on binary64, Python’s numbers, exact ids, money and the limits of other formats. The questions explain themselves as you answer, and keep returning to the idea you find hardest:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): why does JSON.parse(“9007199254740993”) give 9007199254740992?
Every JavaScript number is an IEEE 754 binary64 value, which has 53 bits of precision. Integers are exact up to
2^53 − 1, 9007199254740991, Number.MAX_SAFE_INTEGER; above it, neighbouring binary64 values are 2 apart, so
9007199254740993 has no value of its own. JSON.parse takes the nearest one, and since 9007199254740993 lies exactly
halfway between 9007199254740992 and 9007199254740994, the tie goes to 9007199254740992. No error is raised, so two
different ids can become the same number. Python’s json.loads keeps the integer exact, because Python integers have
no fixed size. The fix is to send such ids as strings, as RFC 7493 recommends, or to read them with the reviver’s
source text where the engine supports it.
Two harder questions follow, about ids that change between a back end and a browser and about totals that do not match a bank’s. Decide what you would check first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A back end sends 64-bit order ids as JSON numbers. In the browser, two orders sometimes show the same id, and a link opens the wrong order. Explain the cause and fix it end to end, for browsers with and without the newest JSON features.
-
Mid to senior
A nightly job reads 2 million line amounts such as 1.15 from a JSON export as floats, adds them up and compares the total with the bank's total in paise. The two differ by a few paise on some nights. Where can the difference come from, and how do you make the comparison exact?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Up to which integer is a JavaScript number exact?
9007199254740991, which is 2^53 − 1 and Number.MAX_SAFE_INTEGER. Past it, neighbouring binary64 values are at least 2 apart.
-
Why does JSON.parse("9007199254740993") return 9007199254740992?
The text sits exactly halfway between two binary64 values, and the tie goes to 9007199254740992. Nothing warns you.
-
How does Python's json module read numbers?
Integers stay exact at any size, and fractions become floats. parse_float=Decimal keeps decimal amounts exact; integers longer than 4,300 digits are refused by default.
-
How can JavaScript read and write a big integer in JSON without rounding it?
With the reviver's context.source, the number's original text, and with JSON.rawJSON, which writes digits as given. Test that the engine has both, and refuse rather than round where it does not.
-
How should an API send amounts of money and 64-bit ids?
Money as whole minor units, such as paise, or as a decimal string with its currency, never as a binary float. 64-bit ids as strings, which is what I-JSON recommends.
-
How many significant digits does Excel keep for a number?
15. A longer number, such as an 18-digit reference, loses its last digits once a spreadsheet reads it as a number.
-
What does TOML require of a parser for 64-bit integers?
To keep each one losslessly or to refuse it: rounding is not an option.
Key takeaways
- JSON numbers are decimal text with no limit, but a binary64 reader such as
JSON.parsekeeps integers exact only up to 2^53 − 1 and rounds the rest, silently, so 9007199254740993 and 9007199254740992 become oneNumber. - Python’s
jsonkeeps integers exact and reads fractions as floats;parse_float=Decimalkeeps decimal amounts exact, and integers of more than 4,300 digits are refused by default. - In ECMAScript 2026 the reviver’s
context.sourceandJSON.rawJSONread and write big integers exactly; test for them, and refuse rather than round where they are missing. - Send money as whole minor units or decimal strings with the currency, never as binary floats, and send 64-bit ids as strings, as I-JSON recommends.
- TOML asks parsers to keep 64-bit integers losslessly or refuse them, CSV leaves every number to its reader, and Excel keeps only 15 significant digits.
Terms in this lesson
- binary64
- The IEEE 754 64-bit floating-point format, also called double precision, with 53 bits of precision. Every JavaScript number and every Python float is a binary64 value.
- minor unit
- The smallest unit of a currency that amounts are counted in, such as the cent or the paisa. Storing amounts as whole minor units keeps every sum exact.
- safe integer
- An integer that binary64 holds exactly and shares with no other integer: from -(2^53 - 1) to 2^53 - 1, which is 9007199254740991. Number.isSafeInteger tests for it.
References
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format, section 6 (IETF)
- RFC 7493: The I-JSON Message Format, section 2.2 (IETF)
- ECMAScript 2026 Language Specification: JSON.parse, JSON.rawJSON and Number.MAX_SAFE_INTEGER (Ecma International)
- Finished proposals: JSON.parse source text access (Ecma TC39)
- JSON.rawJSON() (Mozilla)
- json: JSON encoder and decoder (Python 3.14) (Python Software Foundation)
- Built-in Types: Integer string conversion length limitation (Python 3.14) (Python Software Foundation)
- What's New In Python 3.12: sum() and Neumaier summation (Python Software Foundation)
- TOML v1.1.0: Integer (TOML)
- Excel specifications and limits (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress