5

I need to decode (Decodable protocol) an imprecise decimal value correctly, from this question I understand how to properly handle the Decimal instantiation, but how can I do this when decoding?

If trying to init any number as a String

if let value = try! container.decode(String.self, forKey: .d) {
    self.taxAmount = Decimal(string: value)
}

I get Fatal Error: "Expected to decode String but found a number instead."

And if try to init 130.43 as a Decimal

if let value = try! container.decode(Decimal.self, forKey: .d) {
    //value.description is 130.43000000000002048
    self.d = Decimal(string: value.description)
    //making subtotal to be also 130.43000000000002048 and not 130.43
}

Is there any way to use either of this constructors when decoding?

  • NSDecimalNumber(string: "1.66")
  • NSDecimalNumber(value: 166).dividing(by: 100)
  • Decimal(166)/Decimal(100)
  • Decimal(sign: .plus, exponent: -2, significand: 166)

Here is a simplified version of the JSON I receive from the external service:

{
   "priceAfterTax": 150.00,
   "priceBeforeTax": 130.43,
   "tax": 15.00,
   "taxAmount": 19.57
}

Note: I can't change what is being received to be decoded, I'm stuck working with decimal numbers.

Jose
  • 263
  • 4
  • 14
  • This is actually a big problem which is not handled correctly in most languages. The usual solution is to parse `Double`, store it into `Decimal` and then `round` it to a given number of `Decimal` digits. – Sulthan Mar 12 '19 at 22:14
  • 1
    Sadly you cannot. `JSONDecoder` uses `NSJSONSerialization` internally, which decodes to `Double`. So even if you decode a `Decimal`, it is first internally decoded to a `Double` and hence precision is lost. As Sulthan pointed out, there's a workaround, but there's no real solution due to this implementation issue. – Dávid Pásztor Mar 12 '19 at 22:20
  • You can simply encode and decode it as a String and create a computed property that returns a Decimal – Leo Dabus Mar 12 '19 at 22:27
  • Hi @LeoDabus I can't, I always receive a number, if I could edit the data received believe me that would've been my first try. – Jose Mar 12 '19 at 22:54
  • You can convert the received data to string using number formatter – Leo Dabus Mar 12 '19 at 22:56
  • So you are not encoding the object? If the value you receive is a Double there is nothing you can do other than rounding it yourself – Leo Dabus Mar 12 '19 at 22:57
  • @LeoDabus I'm not encoding the object, I'm receiving it from an external service, I guess I could write some really complicated code to go an edit the json as a string to change the numbers to strings and then decode it, but I'm not sure how would I identify the numbers and enclose them in quotes – Jose Mar 12 '19 at 23:01
  • So all you want is to set the maximum fraction digits to 2 ? – Leo Dabus Mar 12 '19 at 23:08
  • Hi @LeoDabus, I've added a JSON example. I always receive numbers with scale 2, I need to have the exact value in Swift without loosing precision – Jose Mar 12 '19 at 23:12

2 Answers2

9

You can implement your own decoding method, convert your double to string and use it to initialize your decimal properties:


extension LosslessStringConvertible {
    var string: String { .init(self) }
}

extension FloatingPoint where Self: LosslessStringConvertible {
    var decimal: Decimal? { Decimal(string: string) }
}

struct Root: Codable {
    let priceAfterTax, priceBeforeTax, tax, taxAmount: Decimal
}

extension Root {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.priceAfterTax = try container.decode(Double.self, forKey: .priceAfterTax).decimal ?? .zero
        self.priceBeforeTax = try container.decode(Double.self, forKey: .priceBeforeTax).decimal ?? .zero
        self.tax = try container.decode(Double.self, forKey: .tax).decimal ?? .zero
        self.taxAmount = try container.decode(Double.self, forKey: .taxAmount).decimal ?? .zero
    }
}

let data = Data("""
{
"priceAfterTax": 150.00,
"priceBeforeTax": 130.43,
"tax": 15.00,
"taxAmount": 19.57
}
""".utf8)

let decodedObj = try! JSONDecoder().decode(Root.self, from: data)
decodedObj.priceAfterTax   // 150.00
decodedObj.priceBeforeTax  // 130.43
decodedObj.tax             // 15.00
decodedObj.taxAmount       // 19.57
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
-1

Try this:

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let tmpDouble = try container.decode(Double.self, forKey: .yourKey)
    decimalValue = Decimal(string: tmpDouble.description) ?? .zero
}
Andrey M.
  • 3,021
  • 29
  • 42