NSJSONSerialization with small decimal numbers
Manual conversion
You'll need to convert your Double
to a Decimal
to keep its expected string representation when serializing.
One way to avoid a precision of 16 digits may be to round with a scale of 15:
(0.81 as NSDecimalNumber).rounding(accordingToBehavior: NSDecimalNumberHandler(roundingMode: .plain, scale: 15, raiseOnExactness: false, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true)) as Decimal
JSONSerialization extension for automatic conversion
To automatically and recursively do it for all Double
values in your JSON object, being it a Dictionary or an Array, you can use:
import Foundation
/// https://stackoverflow.com/q/35053577/1033581
extension JSONSerialization {
/// Produce Double values as Decimal values.
open class func decimalData(withJSONObject obj: Any, options opt: JSONSerialization.WritingOptions = []) throws -> Data {
return try data(withJSONObject: decimalObject(obj), options: opt)
}
/// Write Double values as Decimal values.
open class func writeDecimalJSONObject(_ obj: Any, to stream: OutputStream, options opt: JSONSerialization.WritingOptions = [], error: NSErrorPointer) -> Int {
return writeJSONObject(decimalObject(obj), to: stream, options: opt, error: error)
}
fileprivate static let roundingBehavior = NSDecimalNumberHandler(roundingMode: .plain, scale: 15, raiseOnExactness: false, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true)
fileprivate static func decimalObject(_ anObject: Any) -> Any {
let value: Any
if let n = anObject as? [String: Any] {
// subclassing children
let dic = DecimalDictionary()
n.forEach { dic.setObject($1, forKey: $0) }
value = dic
} else if let n = anObject as? [Any] {
// subclassing children
let arr = DecimalArray()
n.forEach { arr.add($0) }
value = arr
} else if let n = anObject as? NSNumber, CFNumberGetType(n) == .float64Type {
// converting precision for correct decimal output
value = NSDecimalNumber(value: anObject as! Double).rounding(accordingToBehavior: roundingBehavior)
} else {
value = anObject
}
return value
}
}
private class DecimalDictionary: NSDictionary {
let _dictionary: NSMutableDictionary = [:]
override var count: Int {
return _dictionary.count
}
override func keyEnumerator() -> NSEnumerator {
return _dictionary.keyEnumerator()
}
override func object(forKey aKey: Any) -> Any? {
return _dictionary.object(forKey: aKey)
}
func setObject(_ anObject: Any, forKey aKey: String) {
let value = JSONSerialization.decimalObject(anObject)
_dictionary.setObject(value, forKey: aKey as NSString)
}
}
private class DecimalArray: NSArray {
let _array: NSMutableArray = []
override var count: Int {
return _array.count
}
override func object(at index: Int) -> Any {
return _array.object(at: index)
}
func add(_ anObject: Any) {
let value = JSONSerialization.decimalObject(anObject)
_array.add(value)
}
}
Usage
JSONSerialization.decimalData(withJSONObject: [ "value": 0.81 ], options: [])
Note
If you need fine tuning of decimal formatting, you can check Eneko Alonso answer on Specify number of decimals when serializing currencies with JSONSerialization.
For precise base-10 arithmetic (up to 38 significant digits)
you can use NSDecimalNumber
:
let jsonInput = [ "value": NSDecimalNumber(string: "0.81") ]
or
let val = NSDecimalNumber(integer: 81).decimalNumberByDividingBy(NSDecimalNumber(integer: 100))
let jsonInput = [ "value": val ]
Then
let data = try NSJSONSerialization.dataWithJSONObject(jsonInput, options: NSJSONWritingOptions.PrettyPrinted)
let json = NSString(data: data, encoding: NSUTF8StringEncoding)!
print( json )
produces the output
{
"value" : 0.81
}