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
}

Tags:

Json

Swift