Specify number of decimals when serializing currencies with JSONSerialization Specify number of decimals when serializing currencies with JSONSerialization json json

Specify number of decimals when serializing currencies with JSONSerialization


After doing some research in this matter, a coworker found that rounding the values specifying a behavior using NSDecimalNumberHandler solves the JSON serialization issue.

fileprivate let currencyBehavior = NSDecimalNumberHandler(roundingMode: .bankers, scale: 2, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: true)extension Decimal {    var roundedCurrency: Decimal {        return (self as NSDecimalNumber).rounding(accordingToBehavior: currencyBehavior) as Decimal    }}

Following the example code from the post, we get the desired output:

serialize(prices: Decimal(125.99).roundedCurrency, Decimal(16.42).roundedCurrency, Decimal(88.56).roundedCurrency, Decimal(88.57).roundedCurrency, (Decimal(0.1) + Decimal(0.2)).roundedCurrency)// {"value":[125.99,16.42,88.56,88.57,0.3]}

It works! Ran a test for 10000 values (from 0.0 to 99.99) and found no issues.

If needed, the scale can be adjusted to the number of decimals from the current locale:

var currencyFormatter = NumberFormatter()currencyFormatter.numberStyle = .currencyAccountinglet scale = currencyFormatter.maximumFractionDigits// scale == 2


The problem is that you are using Any as a variadic input parameter to the function instead of using a generic function/overloading the function. This way the exact type information is masked by upcasting to Any.

You have several methods to solve this issue:

  1. Keep the current implementation with Any as the type of the input parameters, but conditional downcast your value inside serialize before printing them and use a NumberFormatter() for the printing.
  2. Change the implementation of serialize to be a generic function.
  3. Implement 3 overloaded versions of serialize, each accepting a different number type and working with the exact types.

If you need your JSON to contain your prices in a certain formatting, you should serialise the output of the NumberFormatter instead of the numbers themselves.


A strategy for encoding currency amounts is to convert the amount to an integer by multiplying the value by a multiplication factor.The multiplication factor in this case is given by the 10 raised to the power of the currency's maximumFractionDigits (e.g. 10^2 for currencies that use two digits in the fractional part).

Keep in mind that this approach is only suitable for storing amounts that are ready to be shown to the user. See here for details.

func serialize(prices: Double..., locale: Locale) {    let formatter = NumberFormatter()    formatter.locale = locale    formatter.numberStyle = .currencyAccounting    // We multiply the amount by as many orders of     // magnitude are needed to ensure it is an integer.    // In your implementation, you should store the value of    // maximumFractionDigits along with the amount to ensure    // you can always recover the correct value.    let items = prices.map {        $0 * pow(10, Double(formatter.maximumFractionDigits))    }    let data = try! JSONSerialization.data(withJSONObject: ["value": items], options: [])    let string = String(data: data, encoding: .utf8)!    print(string)}print(serialize(prices: 125.99, 16.42, 88.56, 88.57, 0.1 + 0.2, locale: Locale(identifier: "en_AU")))

Prints:

{"value":[12599,1642,8856,8857,30]}