Serialising monetary amounts

David
5 min readJun 20, 2021

TL;DR

{
"sourceAmount": "GBP 12.50",
"targetAmount": "JPY 5030",
"negativeExample": "EUR -1460.36"
}

Handling money in your code

If you’ve been developing financial software for a while, you’ll have collected a handful of golden rules that allow good manners to be maintained in the core of your programs:

  • Avoid using floating point types to represent currency amounts. Decimal formats exist in most languages to represent decimal numbers accurately. 🐍
  • If you choose an integer representation of minor currency amounts (pence, cents) then £21,474,836.47 is the largest amount of money you can represent as a number of pence in a signed int (32) primitive. It’s ((2³¹ - 1) / 100). £92,233,720,368,547,758.07 (PayPal ‘credits’ US man $92 quadrillion in error) is the maximum for a long (64). Int (32) is not enough for many cases and long (64) may not suffice in some limited cases 💰💰💰.
  • Without processes in place, such as using objects to encapsulate currency and amount, values of different currencies can be combined in meaningless sums such as £55.10 + $100.20
Currency currency1 = GBP
BigDecimal amount1 = new BigDecimal("55.10")
// not new BigDecimal(55.10) 👎 to floats
Currency currency2 = USD
BigDecimal amount2 = new BigDecimal("100.20")
Currency currency3 = GBP // Picked at random
BigDecimal meaninglessAmount3 = amount1.add(amount2)

Looking further at Java’s BigDecimal, it has some unorthodox behaviour:

new BigDecimal(“1.2”).equals(new BigDecimal(“1.20”));
// results in false due to the scales being different

Using an object to wrap currency and amount and enforce that the scale of the amount is set to the minor scale of the currency prevents unexpected comparison failures. Java Money is a ready-made option, but you may choose to roll your own code if you want something that’s simpler or matches your internal representation more closely.

Serialisation to JSON

Presumably, you’ll also want to send monetary values across to other services and clients within your system. If you’re using a binary format to serialise your values then you need to come up with something sensible that fits your context.

If you’re using a well-established format such as JSON then you might expect that there is an agreed standard for serialisation. There isn’t.

“A large proportion of the computers in the world manipulate money, so it’s always puzzled me that money isn’t actually a first class data type in any mainstream programming language.” — Martin Fowler, Patterns of Enterprise Application Architecture

Amount as JSON number

JSON doesn’t have a decimal datatype so any representation with a decimal point is liable to be treated as a floating point number by an unsuspecting receiving system.

{
"paymentAmount": {
"currency": "GBP",
"amount": 15.56,
"_comment": "👎 will be interpreted by most JSON libraries as a float"
}
}

This might be fine for your purposes, if you have control of the codebase for your iPhone and Android apps and your API is only for internal consumption. You’ll have to take steps to make sure each value arrives at your application layer as the decimal or integer representation that you’ve chosen.

Amount as string

Using a String amount guarantees that no precision will be lost on the way through a JSON library to your application layer; your application will be presented with a String and will need to interpret it.

{
"paymentAmount": {
"currency": "GBP",
"amount": "15.56"
}
}

Enforcing that the amount is always formatted as a string with leading zeros, £15.00 is strictly formatted as “15.00” rather than “15” or “15.0”, has the benefit of implicitly communicating the “minor scale” of 2 for GBP. JPY has a minor scale of 0, so amounts would always be encoded as “123” rather than “123.00”.

Minor amount as JSON integer

If you’ve chosen an integer representation for monetary amounts within your code, it might make sense to expose pence amounts in JSON.

{
"paymentAmount": {
"currency": "GBP",
"amountMinor": 1556,
"minorScale": 2
}
}

This removes any issues around loss of precision, but you may also deem it necessary to specify the minor scale that’s being used to convert the major amount into the minor amount. It’s unlikely that the relationships between pounds and pence or dollars and cents are going to change, but it’s unwise to rely on libraries at the sender and receiver of a JSON message to both be in perfect agreement about the minor scales of all world currencies.

Switching to minor amounts reduces human readability and, depending on how your JSON library models integers, potentially limits the maximum monetary amount that can be represented.

⬆️ All formats where currency is separate from amount

The formats that have been considered so far all maintain a separate currency and amount field. The resulting JSON looks a little messy to human readers. It’s an object representation with two fields and doesn’t feel like a language primitive.

The separate amount may encourage clients to ignore the currency value, read out all the amounts using a JSON query and start processing the amounts based on some assumptions: “I’m sure all the values are GBP…”.

And the winner is…

If we’ve decided to use a string attribute for amount and our application is forced to parse the value anyway, we can combine the currency and amount into a single string of: ISO currency code, followed by a space, followed by a signed number with decimal places matching the currency’s minor scale.

🥇🥇🥇🥇🥇🥇🥇🥇

{
"paymentAmount": "GBP 15.56"
}

🥇🥇🥇🥇🥇🥇🥇🥇

Application layer

If your chosen JSON library allows converters to be plugged in so that “EUR 123.45” can be converted directly to an object that you’re using to represent monetary amounts, then it’s probably worth the investment in time to write the plugin. Your data objects will be automatically serialised and deserialised using your chosen format.

Tidy up questions

Why an ASCII decimal point not a comma?

  • European languages tend to use a comma to separate the major and minor currency amounts. For example, “€12.34” is normally formatted in France as “12,34 €”. ASCII decimal points are used in source code for mainstream programming languages, making it a natural first choice.

Why no grouping of the major amount into thousands and millions?

  • In many languages, large numbers are grouped with a comma like “£123,345.78”. This behaviour is locale-specific and adds unnecessary complication to a straightforward, unadorned number-as-string representation in JSON.

Why 3-letter ISO currency codes rather than symbols?

  • Symbols are locale-specific. ISO codes, while not looking as good on the page, are unambiguous.

Why have the ISO currency code at the start rather than the end?

  • Knowing that the first 3 characters are going to be the ISO code, followed by a space and then followed by the numerical amount makes the string easy to parse using fixed positions from left to right. Of course, you could make the opposite argument if you prefer parsing strings from the end to the beginning…

Isn’t asking the application layer to parse a string unnecessary extra work?

  • If handling monetary amounts correctly is important to your application then it’s a good opportunity to stop JSON parsers using their default behaviour and specify what behaviour you want.

I’m already using a different format. How should I switch if I want to?

  • The general rule of serialisation is to make your serialiser strict so that it always represents values in a single way but to make your deserialiser tolerant and forgiving. Start by ensuring that all deserialisers in your system can handle both your existing format and the new format. Once in place, change your serialisers one by one to generate the new format.

--

--