Objectmapper: Decimal 0.691 is not accurate even after the fix for issue #777

Created on 20 Apr 2017  路  5Comments  路  Source: tristanhimmelman/ObjectMapper

Your JSON dictionary:

{
  "quantity": 0.691
}

Your model:

struct Repo: Mappable {
  var quantity: NSDecimalNumber?

  init(_ map: Map) {
    quantity <- (map["quantity"], NSDecimalNumberTransform())
  }
}

What you did:

let repo = Mapper<Repo>().map(myJSONDictionary)

What you expected:

I exepected something like:

Repo(quantity: 0.691)

What you got:

Repo(quantity: 0.6909999999999999)  // expected the quantity is 0.691

Most helpful comment

I made my own DecimalNumbertransform function like this. I'm not good at Swift. Just to share what I did to workaround my own problem.

import Foundation
import ObjectMapper

open class MyDecimalNumberTransform: TransformType {
    public typealias Object = NSDecimalNumber
    public typealias JSON = String

    public init() {}

    public func transformFromJSON(_ value: Any?) -> NSDecimalNumber? {
        if let string = value as? String {
            return NSDecimalNumber(string: string)
        } else if let number = value as? NSNumber {
            let handler = NSDecimalNumberHandler(roundingMode: .plain, scale: 3, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false)
            return NSDecimalNumber(decimal: number.decimalValue).rounding(accordingToBehavior: handler)
        } else if let double = value as? Double {
            return NSDecimalNumber(floatLiteral: double)
        }
        return nil
    }

    public func transformToJSON(_ value: NSDecimalNumber?) -> String? {
        guard let value = value else { return nil }
        return value.description
    }
}

Mapping in the model like

struct Repo: Mappable {
  var quantity: NSDecimalNumber?

  init(_ map: Map) {
    quantity <- (map["quantity"], MyDecimalNumberTransform())
  }
}

All 5 comments

Unfortunately this is not caused by this library. After some investigation I have found that NSJSONSerialization will decode numbers as doubles before falling back on NSDecimalNumber when the value is too large to fit into the Double type, as described here: http://stackoverflow.com/a/39553617

Essentially the first step from converting JSON data to a Swift Dictionary [String: Any] introduces the Double type, and this is then passed into this library to map. From there it is converting Double to NSDecimalNumber, but the precision is already lost.

The result is that if you require precision you should encode your numbers as Strings in JSON.

let jsonString = "{\"quantity\": 0.691}"
let data = jsonString.data(using: .utf8)!
let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
let value = json!["quantity"]!

print("\(type(of: value)) \(value)")
// prints __NSCFNumber 0.6909999999999999

Using a String to hold the JSON number, and using your example to map the model into an object instead yields:

let jsonString = "{\"quantity\": \"0.691\"}"
let repo = Mapper<Repo>().map(JSONString: jsonString)
let value = repo!.quantity!

print("\(type(of: value)) \(value)")
// prints NSDecimalNumber 0.691

Thanks @Jake000 . Wonderful explanation! The limitation is actually in the JSON data type definition. Number in JSON is defined as double-precision floating-point.

If I want to code a workaround for my case, and the decimal number is always 3-decimal place or less. How can I add the 3-decimal place rounding from Double to NSDecimalNumber in my own NSDecimalNumberTransform()?

MyNSDecimalNumberTransform.swift

     open func transformFromJSON(_ value: Any?) -> NSDecimalNumber? {
          if let string = value as? String {
              return NSDecimalNumber(string: string)
          }
          if let double = value as? Double {
        } else if let number = value as? NSNumber {
            return NSDecimalNumber(decimal: number.decimalValue)
        } else if let double = value as? Double {
              return NSDecimalNumber(floatLiteral: double)
          }
          return nil

The best solution would be to use Strings to hold numbers in JSON.

Using the previous example, the first option would be much more preferable to the second as it will be decoded as a String which can be converted lossless to a decimal number.

{ "quantity": "0.691" }

```JSON
{ "quantity": 0.691 }

If you are not able to change the incoming JSON (eg. it is from a service you do not own) then you can always round the result to 3 decimal places.

```Swift
extension NSDecimalNumber {
    func rounded(places: Int) -> NSDecimalNumber {
        var decimalValue = self.decimalValue
        var result: Decimal = 0
        NSDecimalRound(&result, &decimalValue, places, .plain)
        return NSDecimalNumber(decimal: result)
    }
}

let jsonString = "{\"quantity\": 0.691}"
let data = jsonString.data(using: .utf8)!
let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
let value = json!["quantity"] as! Double
let decimal = NSDecimalNumber(value: value)
let rounded = decimal.rounded(places: 3)

print("\(type(of: rounded)) \(rounded)")
// prints NSDecimalNumber 0.691

You can wrap this up in your own transform by subclassing the existing:

open class RoundedDecimalNumberTransform: NSDecimalNumberTransform {
    open override func transformFromJSON(_ value: Any?) -> NSDecimalNumber? {
        return super.transformFromJSON(value)?.rounded(places: 3)
    }
}

Thanks @Jake000 !!

I made my own DecimalNumbertransform function like this. I'm not good at Swift. Just to share what I did to workaround my own problem.

import Foundation
import ObjectMapper

open class MyDecimalNumberTransform: TransformType {
    public typealias Object = NSDecimalNumber
    public typealias JSON = String

    public init() {}

    public func transformFromJSON(_ value: Any?) -> NSDecimalNumber? {
        if let string = value as? String {
            return NSDecimalNumber(string: string)
        } else if let number = value as? NSNumber {
            let handler = NSDecimalNumberHandler(roundingMode: .plain, scale: 3, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: false)
            return NSDecimalNumber(decimal: number.decimalValue).rounding(accordingToBehavior: handler)
        } else if let double = value as? Double {
            return NSDecimalNumber(floatLiteral: double)
        }
        return nil
    }

    public func transformToJSON(_ value: NSDecimalNumber?) -> String? {
        guard let value = value else { return nil }
        return value.description
    }
}

Mapping in the model like

struct Repo: Mappable {
  var quantity: NSDecimalNumber?

  init(_ map: Map) {
    quantity <- (map["quantity"], MyDecimalNumberTransform())
  }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

VictorAlbertos picture VictorAlbertos  路  3Comments

nearspears picture nearspears  路  4Comments

loryhuz picture loryhuz  路  4Comments

jperera84 picture jperera84  路  4Comments

patchthecode picture patchthecode  路  3Comments