The safest way to store money amounts in JSON is as a decimal string, such as "1249.95", or as an integer count of minor units, such as 124995 cents, always with a currency code next to it. Plain decimal numbers look fine, but most languages read them as binary floating point, so sums drift.
Why do JSON money amounts drift by a cent?
JSON money amounts drift because most JSON parsers turn a number like 0.1 into a binary floating-point value, and binary floating point cannot hold most decimal fractions exactly. Each amount is stored as the nearest value the format can represent, and when you add many of them the tiny errors add up and eventually show in the cents.
The best-known case is short. In JavaScript, and in any language using the same double-precision numbers, 0.1 plus 0.2 does not equal 0.3. The result is 0.30000000000000004. On its own that is harmless, because rounding to two places gives 0.30. The trouble starts when code compares amounts for equality, adds thousands of lines, or rounds at the wrong moment.
A worked sum
Here is an example of how it bites in practice. A reconciliation script reads three payments from a JSON file and checks that they match a deposit of 0.60.
- The JSON holds the payments as numbers: 0.10, 0.20 and 0.30.
- The script adds them as doubles and gets 0.6000000000000001.
- It compares the sum with 0.60 and reports a mismatch that does not exist.
Nobody made a mistake typing those numbers. The format did what it was designed to do. The JSON standard itself is careful here. RFC 8259, the JSON specification, says implementations get good interoperability by expecting no more precision or range than IEEE 754 double-precision numbers provide, and that integers are interoperable only within plus or minus 2 to the power 53, minus one. In other words, the standard does not promise that a reader will keep your decimals exact.
Three ways to write a money amount in JSON
There are three common ways to write a money amount in JSON: as a plain number with decimals, as a decimal string in quotes, or as an integer number of minor units such as cents. All three are valid JSON. They differ in what a typical reader does with them, which is where rounding errors are either born or avoided.
The table compares one example amount, one thousand two hundred forty-nine dollars and ninety-five cents, written each way.
| Format | JSON value | What a typical parser gives you | Exact? |
|---|---|---|---|
| Number with decimals | 1249.95 |
A double close to 1249.95 | No, approximate |
| Decimal string | "1249.95" |
Text, which you convert with a decimal type | Yes, if converted with a decimal type |
| Minor units | 124995 |
An integer | Yes, within the safe integer range |
Numbers with decimals
This is the format most people write first, because it looks like the amount. It also works for display and for rough totals. It fails quietly in sums, comparisons and anything that must match a bank to the cent.
Decimal strings
A decimal string keeps the exact digits, including trailing zeros such as "40.00". The reader parses the string with a decimal type, such as Python's Decimal, Java's BigDecimal or a decimal library in JavaScript. The cost is that a careless reader treats it as text, and string "sums" concatenate instead of add.
Minor units
Minor units means counting in the smallest unit of the currency: cents for dollars, pence for pounds. The value 124995 is exact in every language, and integer arithmetic never drifts. The cost is that a reader must know how many decimal places the currency has before showing the amount, and that rule is not the same for every currency.
Which JSON money format should you pick?
Pick minor units when the file stays between systems you control and every reader knows each currency's decimal places. Pick decimal strings when other people's code reads the file, or when amounts can carry more decimals than the currency's usual two. Avoid plain decimal numbers for anything that will be summed, compared or reconciled.
A few situations push the choice one way or the other:
- Payment and banking APIs. Many use minor units, because integers travel safely through every language. Follow the API you are talking to rather than inventing a new rule.
- Exports for people and spreadsheets. Decimal strings read naturally, convert cleanly to a currency column, and do not need a lookup table for decimals. If the file feeds a workbook such as a bank statement Excel template, "1249.95" maps straight onto a currency cell.
- Unit prices and exchange rates. These often need four or more decimals. Minor units cannot hold a price of 0.0375 dollars per item; a decimal string can.
- Very large values. In minor units, the safe integer limit from RFC 8259 is about 90 trillion dollars, which is enough for nearly everything. Some currencies with large nominal values get closer, so check before relying on it.
Whatever you pick, write it down in the file's documentation or schema, and use it for every amount in the file. A file that mixes "12.50" in one field and 1250 in another is worse than either choice alone.
Currency codes next to every JSON amount
Every money amount in JSON needs a currency code beside it or in a parent object that clearly covers it. The standard codes are the three-letter ISO 4217 codes, such as USD, EUR and GBP. Without a code, 124995 could be dollars in cents, yen in whole units or anything else, and no reader can tell.
The list of codes is kept by an official maintenance agency. SIX, the ISO 4217 maintenance agency, publishes the current lists, including each currency's minor unit, which is the number of decimal places it uses. The euro, for example, has a minor unit of 2. That figure is what a reader needs to turn minor units back into a displayed amount.
Two patterns are common:
- A money object:
{ "amount": "1249.95", "currency": "USD" }. Each amount carries its own code, which suits files that mix currencies. - A code at the top: one
"currency": "USD"in the statement header, with plain amount fields below. This suits a single-account file, such as one bank statement, where every line is in the same currency.
Do not use currency symbols in place of codes. The dollar sign covers many currencies, and some symbols do not survive a trip through a badly encoded file. The code is short, plain ASCII and unambiguous.
Signs, zero and missing money values in JSON
A refund, a zero amount and an unknown amount are three different things, and JSON can show all three clearly: a minus sign for money going the other way, zero written as "0.00" or 0, and null for a value that is missing. Writing 0 for an unknown amount makes totals look right when they are not.
Negative amounts
Use a leading minus for money leaving the account, or for refunds, depending on the side you write from. State the rule once. Do not invent extra fields such as a separate "isDebit" flag next to a positive amount unless an API requires it, because the flag and the amount can disagree.
Zero and null
Zero means "nothing was charged". Null means "we do not know". A converted bank statement line with no printed amount, such as a balance-forward row, should have null, not zero, so that a script counting transactions can tell the difference.
Implied decimals in old formats
Older bank files sometimes store amounts with no decimal point at all, such as 000004128 for 41.28, with the decimal places implied by a column specification. If you are building JSON from one of those, convert the value with the documented number of places, not by guessing. Fixed-width text in Excel covers how those files lay out their columns.
Money amount mistakes that slip past a JSON parser
Some bad money values are perfectly valid JSON, so the parser accepts them and the error surfaces only later in a report or a reconciliation. Thousands separators in strings, commas used as decimal points, mixed formats in one file and amounts rounded before they were written are the common ones. A validator that knows your money rules catches them early.
Look out for these:
- Thousands separators. "1,249.95" is a valid string, but many decimal parsers reject it or read only the 1. Strip separators before writing.
- Decimal commas. "1249,95" is how many European statements print amounts. In a JSON string it reads as text, and a careless conversion may give 124995 or 1249. Normalise to a decimal point.
- Trailing zeros dropped. Writing 40 when the amount was 40.00 is harmless in a number, but in a decimal string, "40" and "40.00" can look like different precision to some readers. Write the full two places.
- Rounded before writing. If an amount was calculated, such as a share of a split bill, round it once with a clear rule before it goes into the file, not separately in every reader.
- Positional amounts with no labels. Data that arrived from a CSV without headers sometimes swaps amount and balance columns. Check where each value came from; CSV files without a header row explains how to identify the columns.
FastBankConvert is getting ready to open with conversion of PDF bank statements to Excel, CSV or JSON, and it is built to warn you before download when the balances do not add up. You can get started to hear when it opens.