{
"quantity": 0.691
}
struct Repo: Mappable {
var quantity: NSDecimalNumber?
init(_ map: Map) {
quantity <- (map["quantity"], NSDecimalNumberTransform())
}
}
let repo = Mapper<Repo>().map(myJSONDictionary)
I exepected something like:
Repo(quantity: 0.691)
Repo(quantity: 0.6909999999999999) // expected the quantity is 0.691
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())
}
}
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.
Mapping in the model like