swiftdecodablensdecimalnumber

Swift: Decode imprecise decimal correctly


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?

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.


Solution

  • 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