The problem
JSON only supports float values as per the IEEE-754 standard. And IEEE 754 standard is terrible for representing most floats.
Here is a classic example:
>> 0.1+0.2
0.30000000000000004
The fundamental reason is simple - financial numbers are in powers of 10 (decimal), computer-stored numbers are in powers of 2 (binary). Powers of 2 cannot be used to accurately represent powers of 10, so there is always some rounding and some inaccuracy.
As a rule of thumb, don't use JSON to send fractional values. You can use JSON for floats only if:
- You exactly know the limitations of IEEE-754 standard
- You are completely sure that your use case is okay with the limitations
For financial APIs, IEEE-754 is not at all okay. The standard cannot represent fractional values properly.
So, it cannot represent fractional amounts like $0.1
. And your amounts are going to be fractional most of the time
Solution
The solution is simple - get rid of the float data type from your JSON, and only use integer
or string
datatypes in the JSON-communication layer.
Look at the financial APIs around you. They are all using integer
or string
in their JSON.
- Stripe, Razorpay use
integer
to represent amounts in their API - PayPal uses
string
to represent amounts in its API
In case you find float in a financial API's JSON, you're looking at an API that is going to cause inaccuracies.
HOW TO DO IT?
Converting the float into an integer is almost an industry standard. To do it, represent the amount in its currency's lowest denomination. Examples:
- Smallest divisible unit of a rupee is a paisa. So,
₹22.33
becomes2233 paise
. - Smallest divisible unit of a dollar is a cent. So,
$11.22
becomes1122 cents
.
Convert everything into int
or string
during communication, to prevent loss of information due to limited precision of the IEEE-754 float
(int is preferred). Then, after receiving the JSON, convert the amount back into whatever datatype within your programs.
If you are wondering what is needed for accurately processing and storing financial numbers, or fractions in general , hare are 2 solutions:
- During processing, you can use datatypes with arbitrary (unlimited) precision. For example, java
BigInteger
. - During storage, use datatypes that support fixed decimal values. For example:
- SQLServer can use datatypes with definable precision like
decimal
,numeric
. - Postgres also has
numeric
/decimal
where you define the scale. Postgres also has amoney
datatype, but ironically, it is not recommended. (link :pg - don't use money).
- SQLServer can use datatypes with definable precision like
And from experience, and after talking to too many people about this during the 2021-2022 period, just store money as integer in the smallest denomination. If you really need floats, decimal datatype with 4 decimal digits will solve most use cases (the internet favourite is DECIMAL(19, 4)
).
And on an ending note, IEEE 754 is the reason why I cannot fathom why there are companies writing financial APIs in javascript (Node JS). That language has only one numeric datatype - double precision float (IEEE 754). Doesn't even have int!