0

I have a Django backend that sends python decimal values as string (precision). I'm trying to encode decimal data into Double in my Swift app.

Here is my struct:

public struct MyStruct: Encodable, Decodable {
    
    public var amount: Double?

    public init(amount: Double? = nil) {
        self.amount = amount
    }
}

Possible JSONs:

{
    "amount" = null
}

or:

{
    "amount" = "3187.498149184"
}

When I try to encode my struct I get this error:

Expected to decode Double but found a string/data instead.

What is the the most easiest way to do this? I have a lot of struct models containing Double values so I'm looking for a simple solution.

Paul Bénéteau
  • 775
  • 2
  • 12
  • 36
  • 1
    Are you sure you want to be using `Double` on the Swift end? Presumably, the Django backend isn't encoding its numbers as strings just because they felt like it; they probably wanted to benefit from the decimal-precision that comes from spelling out characters in decimal notation. That might be intentional. You might want to use a datatype that preserves this precision that they were trying to protect. – Alexander Sep 13 '21 at 20:33
  • please post possible json in post. so we can get better idea – SPatel Sep 13 '21 at 21:10
  • @SPatel, added JSON. – Paul Bénéteau Sep 13 '21 at 21:14
  • @PaulBénéteau If you would like to preserve the fraction digits of your amount you should use Swift Decimal type. Make sure also to always use its string initializer. – Leo Dabus Sep 13 '21 at 23:35
  • You can use the string encoding approach shown [here](https://stackoverflow.com/a/62997953/2303865). It would result in `MyStruct(amount: nil)`and `MyStruct(amount: Optional(3187.498149184))`. Just make sure to use Decimal instead of Double in your custom structure – Leo Dabus Sep 13 '21 at 23:47

2 Answers2

0

Wrap your precise values into a custom struct with custom decoding:

public struct PreciseNumber: Codable {
   public let value: Decimal

   private static let posixLocale = Locale(identifier: "en_US_POSIX")

   public init(from decoder: Decoder) throws {
       let container = try decoder.singleValueContainer()
       let rawValue = try container.decode(String.self)

       value = NSDecimalNumber(string: rawValue, locale: Self.posixLocale) as Decimal
   }

   public func encode(to encoder: Encoder) throws {
       var container = encoder.singleValueContainer()
       try container.encode((value as NSDecimalNumber).description(withLocale: Self.posixLocale))
   }
}

Then you directly decode PreciseNumber:

public var amount: PreciseNumber?

I have intentionally used Decimal instead of Double because a Double would lose precision.

A slightly more complex solution is to use a property wrapper:

@propertyWrapper
public struct NumberAsString: Codable {
    public var wrappedValue: Decimal?

    private static let posixLocale = Locale(identifier: "en_US_POSIX")

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawValue = try container.decode(String.self)
    
        guard !container.decodeNil() else {
            wrappedValue = nil
            return
        }

        wrappedValue = NSDecimalNumber(string: rawValue, locale: Self.posixLocale) as Decimal
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        guard let wrappedValue = wrappedValue else {
            try container.encodeNil()
            return
        }

        try container.encode((wrappedValue as NSDecimalNumber).description(withLocale: Self.posixLocale))
    }

    public init(wrappedValue: Decimal?) {
        self.wrappedValue = wrappedValue
    }
}

used as:

public struct MyStruct: Codable {
    @NumberAsString public var amount: Decimal?

    public init(amount: Decimal? = nil) {
        self.amount = amount
    }
}
Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • I get this error: Cannot convert value of type 'Decimal' to expected argument type 'String?' – Paul Bénéteau Sep 13 '21 at 18:55
  • Maybe it is: value = NSDecimalNumber(string: rawValue).decimalValue – Paul Bénéteau Sep 13 '21 at 18:55
  • 1
    I have fixed it. – Sulthan Sep 13 '21 at 18:57
  • I would prefer using Double tho – Paul Bénéteau Sep 13 '21 at 20:11
  • @PaulBénéteau You can change the implementation to `Double` easily. Parse/encode the values using `NumberFormatter`. However, I do not see the point to use `String` to keep precision and then lose it when parsing/encoding. – Sulthan Sep 13 '21 at 20:14
  • Ok. What is Locale(identifier: "en_US_POSIX") used for? – Paul Bénéteau Sep 13 '21 at 20:17
  • 1
    @PaulBénéteau `NSDecimalNumber` can parse values with different locales (e.g. decimal comma instead of decimal point). Using the English-US POSIX locale forces it to use the standard data format. You should use the same locale with `NumberFormatter` when parsing numbers in the standard format. – Sulthan Sep 13 '21 at 20:21
  • Ok I just tried your code with property wrapper but I get this error: Expected String but found null value instead. Which is normal because some values in my json are null, how can your code handle null values? – Paul Bénéteau Sep 13 '21 at 20:39
  • @PaulBénéteau give me a moment, I will update it. – Sulthan Sep 13 '21 at 20:57
  • I'm still getting the same error. – Paul Bénéteau Sep 13 '21 at 21:42
  • @PaulBénéteau Actually, that's actually caused by values that are not present at all, not `null`. Unfortunately, it seems this is now unresolvable using property wrappers since the property value is considered as a non-optional with wrapped optional value and the parsing fails already in `MyStruct` when searching for the key `amount`. I strongly recommend to use the first solution, with a normal wrapping type. – Sulthan Sep 14 '21 at 08:22
0

Just change your property type to string and add custom getter to get it in Double.

public struct MyStruct: Encodable, Decodable {
    
    private var amount: String?

    public var getAmount: Double? {
        if let amount = amount {
            return Double(amount)
        }
        return nil
    }

    public init(amount: String? = nil) {
        self.amount = amount
    }
}

OR

public struct Amount: Codable {
    public let value: String?
    
    public var toDouble: Double? {
        if let val = value { return Double(val)}
        return nil
    }
    
    public init(amount: Double? = nil) {
        if let val = amount {
            self.value = String(val)
        } else {
            self.value = nil
        }
    }
    
    public init(from decoder: Decoder) throws {
        value = try decoder.singleValueContainer().decode(String.self)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value)
    }
}

public struct MyStruct: Encodable, Decodable {
    
    public var amount: Amount?

    public init(amount: Double? = nil) {
        self.amount = Amount(amount: amount)
    }
}

let JSONString = """
        {
            "amount": "3187.498149184",
        }
"""


do {
    let result = try JSONDecoder().decode(MyStruct.self, from: Data(JSONString.utf8))
    print(result.amount?.value)
}
catch let error {
    print(error)
}
SPatel
  • 4,768
  • 4
  • 32
  • 51