NSJSONSerialization with small decimal numbers - json

I'm attemptin to include the Double value of 0.81 in some JSON generated by NSJSONSerialization. The code is as follows:
let jsonInput = [ "value": 0.81 ]
let data = try NSJSONSerialization.dataWithJSONObject(jsonInput, options: NSJSONWritingOptions.PrettyPrinted)
let json = NSString(data: data, encoding: NSUTF8StringEncoding)!
print( json )
The output is:
{
"value" : 0.8100000000000001
}
But what I'd like to see is:
{
"value" : 0.81
}
How can I make NSJSONSerialization do this?
One further thing that is confusing me here is Swift's handling of the 64bit Double. As in the playground I can also do this:
let eightOne:Double = 0.81
"\(eightOne)"
print( eightOne )
And the output is then as desired with:
0.81
Even though in the playground it shows eightOne as 0.8100000000000001 as far as internal representation goes. However here when it converts to string it chops off the rest.
I'm surely this is solved, as you'd need it sorted for any kind of financial handling (eg. in Java we know we only use BigDecimals when it comes to financial values).
Please help. :)
NOTE: The focus here is on serialization to JSON. Not just a simple call off to NSString( format: "%\(0.2)f", 0.81).

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
}

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.

If you have use 'NSDecimalNumber' demand, it is suggested that encapsulate for ease of use and reduce mistakes.
Here's the Demo for you reference, using a simple.The hope can help you!
switch (operatorType) {
case 0:
resultNumber = SNAdd(_cardinalNumberTextField.text, _complementNumberTextField.text);
break;
case 1:
resultNumber = SNSub(_cardinalNumberTextField.text, _complementNumberTextField.text);
break;
case 2:
resultNumber = SNMul(_cardinalNumberTextField.text, _complementNumberTextField.text);
break;
case 3:
resultNumber = SNDiv(_cardinalNumberTextField.text, _complementNumberTextField.text);
break;
}
Github:https://github.com/ReverseScale/DecimalNumberDemo

Related

Decoding JSON strings containing arrays

I'm encoding and decoding to and from JSON strings using JSONSerialization in the class below. I can encode both NSDictionaries & NSArrays and I can decode strings that have been encoded using NSDictionaries but not strings that were encoded from arrays, it barfs at JSONSerialization.jsonObject( ...
I can work without arrays, at a pinch but it would be nice to know why this is happening. Thoughts appreciated
let a = [1,2,3,4,5]
let s = JSON.Encode( a )!
JSON.Decode( s ) // fails
let a = ["a" : 1, "b" : 2, "c" : 3 ]
let s = JSON.Encode( a )!
JSON.Decode( s ) // works
-
class JSON: NSObject {
static func Encode( _ obj: Any ) -> String? {
do {
let data = try JSONSerialization.data(withJSONObject: obj, options:JSONSerialization.WritingOptions(rawValue: 0) )
if let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
return string as String
}
return nil
} catch let error as NSError {
return nil
}
}
static func Decode( _ s: String ) -> (NSDictionary?) {
let data = s.data(using: String.Encoding.utf8, allowLossyConversion: false)!
do {
// fails here to work on anything other than "{ a : b, c : d }"
// hates any [1,2,3] arrays in the string
let json = try JSONSerialization.jsonObject(with: data, options:JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
return json
} catch let error as NSError {
return nil
}
}
}
You are casting the result to dictionary so it cannot work with something else.
A solution is to make the function generic and specify the expected return type.
I cleaned up the code a bit, basically it’s recommended to make a function throw if it contains a throwing API.
class JSON {
enum JSONError : Error { case typeMismatch }
static func encode( _ obj: Any ) throws -> String {
let data = try JSONSerialization.data(withJSONObject: obj)
return String(data: data, encoding: .utf8)!
}
static func decode<T>( _ s: String ) throws -> T {
let data = Data(s.utf8)
guard let result = try JSONSerialization.jsonObject(with: data) as? T else {
throw JSONError.typeMismatch
}
return result
}
}
let a = [1,2,3,4,5]
do {
let s = try JSON.encode(a)
let u : [Int] = try JSON.decode(s)
print(u)
} catch { print(error) }
Note (as always):
In Swift do not use NSDictionary, NSString and NSErrorand .mutableContainers is completely pointless.

Vapor JSON from `[String: Any]` Dictionary

If I build a Swift dictionary, i.e. [String: Any] how can I return that as JSON? I tried this, but it gives me the error: Argument labels '(node:)' do not match any available overloads.
drop.get("test") { request in
var data: [String: Any] = [:]
data["name"] = "David"
data["state"] = "CA"
return try JSON(node: data)
}
Convoluted as heck, but this allows you to use [String:Any].makeNode(), as long as the internals are NodeRepresentable, NSNumber based, or NSNull :) --
import Node
enum NodeConversionError : LocalizedError {
case invalidValue(String,Any)
var errorDescription: String? {
switch self {
case .invalidValue(let key, let value): return "Value for \(key) is not NodeRepresentable - " + String(describing: type(of: value))
}
}
}
extension NSNumber : NodeRepresentable {
public func makeNode(context: Context = EmptyNode) throws -> Node {
return Node.number(.double(Double(self)))
}
}
extension NSString : NodeRepresentable {
public func makeNode(context: Context = EmptyNode) throws -> Node {
return Node.string(String(self))
}
}
extension KeyAccessible where Key == String, Value == Any {
public func makeNode(context: Context = EmptyNode) throws -> Node {
var mutable: [String : Node] = [:]
try allItems.forEach { key, value in
if let _ = value as? NSNull {
mutable[key] = Node.null
} else {
guard let nodeable = value as? NodeRepresentable else { throw NodeConversionError.invalidValue(key, value) }
mutable[key] = try nodeable.makeNode()
}
}
return .object(mutable)
}
public func converted<T: NodeInitializable>(to type: T.Type = T.self) throws -> T {
return try makeNode().converted()
}
}
With that header you can:
return try JSON(node: data.makeNode())
JSON cannot be initialized from a [String : Any] dictionary because Any is not convertible to Node.
There are only a limited number of types that Node can be. (See Node source). If you know your objects are all going to be the same type, use a dictionary that only allows that type. So for your example, [String : String].
If you're going to be getting data from the request, you can try using request.json as is used in the documentation here.
EDIT:
Another (possibly better) solution would be to make your dictionary [String: Node] and then you can include any type that conforms to Node. You may have to call the object's makeNode() function to add it to the dictionary though.

Parsing json to swift constants

I am trying to parse all the data from the endpoint and assign it to constants so I can use them in my ViewController class. My biggest problem is assigning each item of the "key" array to constants. Can anyone help me out?
static func fetchRates(completionHandler: (current: [Currency]) -> ()) {
let urlString = "https://api.bitcoinaverage.com/ticker/all"
let url = NSURL(string: urlString)
NSURLSession.sharedSession().dataTaskWithURL(url!, completionHandler: { (location, response, error) -> Void in
do {
let json = try(NSJSONSerialization.JSONObjectWithData(location!, options: .MutableContainers))
let tickerData = [Currency]()
for key in json as! [String : AnyObject] {
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completionHandler(current: tickerData)
})
} catch let err as NSError {
print(err)
}
}).resume()
}
This is the response from the endpoint.
{
"AUD": {
"24h_avg": 621.17,
"ask": 624.12,
"bid": 620.3,
"last": 620.45,
"timestamp": "Mon, 23 May 2016 20:01:16 -0000",
"total_vol": 671.28
},
"BRL": {
"24h_avg": 1725.77,
"ask": 1748.83,
"bid": 1731.8,
"last": 1738.64,
"timestamp": "Mon, 23 May 2016 20:01:16 -0000",
"total_vol": 534.19
},
"CAD": {
"24h_avg": 579.2,
"ask": 579.27,
"bid": 573.57,
"last": 577.42,
"timestamp": "Mon, 23 May 2016 20:01:16 -0000",
"total_vol": 413.81
},
Give this a try:
for (key, value) in (json as! [String: AnyObject]) {
let currencyData = value as! [String: AnyObject]
// key is the currency code: AUD, BRL, etc
// currencyData is the details: ask, bid, last, ...
}
Here's a rough and ready solution, but it's a start you can build on.
Let's start by defining some convenience types:
// Define a handy result type
// See https://github.com/antitypical/Result if you want a more detailed implementation.
public enum Result<T> {
case Success(T)
case Error(ErrorType)
}
// Define an error type for inconsistentData
public enum DataError: ErrorType {
case NoData
}
// Define a struct to hold a Currency
public struct Currency: CustomStringConvertible, Equatable {
let currencyCode: String
let dayAverage: Double
let ask: Double
let bid: Double
let last: Double
let timestamp: String
let totalVolume: Double
// This makes it easier to debug
public var description: String {
return "currencyCode: \(currencyCode), dayAverage: \(dayAverage), ask: \(ask), bid: \(bid), last: \(last), timestamp: \(timestamp), totalVolume: \(totalVolume)"
}
public init(currencyCode: String,
dayAverage: Double,
ask: Double,
bid: Double,
last: Double,
timestamp: String,
totalVolume: Double) {
self.currencyCode = currencyCode
self.dayAverage = dayAverage
self.ask = ask
self.bid = bid
self.last = last
self.timestamp = timestamp
self.totalVolume = totalVolume
}
}
// Make sure your structs conform to Equatable.
public func == (lhs: Currency, rhs: Currency) -> Bool {
return
lhs.currencyCode == rhs.currencyCode &&
lhs.dayAverage == rhs.dayAverage &&
lhs.ask == rhs.ask &&
lhs.bid == rhs.bid &&
lhs.last == rhs.last &&
lhs.timestamp == rhs.timestamp &&
lhs.totalVolume == rhs.totalVolume
}
// I like to define convenience initialisers outside of the struct. Keeps things less coupled.
public extension Currency {
public init?(currencyCode: String, values: [String : AnyObject]) {
guard
let dayAverage = values["24h_avg"] as? Double,
let ask = values["ask"] as? Double,
let bid = values["bid"] as? Double,
let last = values["last"] as? Double,
let timestamp = values["timestamp"] as? String,
let totalVolume = values["total_vol"] as? Double
else { return nil }
self = Currency(currencyCode: currencyCode,
dayAverage: dayAverage,
ask: ask,
bid: bid,
last: last,
timestamp: timestamp,
totalVolume: totalVolume)
}
}
Now the function to fetch the values can be written as:
// The function to fetch the currencies.
// Use a richer type for the completion parameter. It is either a success with a list of currencies or an error.
public func fetchRates(completionHandler: (Result<[Currency]>) -> ()) {
let url = NSURL(string: "https://api.bitcoinaverage.com/ticker/all")! // Force unwrapped as we assume this is a valid URL
NSURLSession.sharedSession().dataTaskWithURL(url) { (data, _, error) in
if let error = error {
completionHandler(.Error(error))
return
}
do {
guard
let data = data,
// Being generous with the type of the result because there is a top level key that is the timestamp not related to a currency.
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers) as? [String : AnyObject]
else {
completionHandler(.Error(DataError.NoData))
return
}
var currencies = [Currency]()
for (key, value) in json {
guard
// Make sure that the values we are about to pass on are of the correct type.
let value = value as? [String : AnyObject],
let currency = Currency(currencyCode: key, values: value)
else { continue }
currencies.append(currency)
}
print(currencies)
completionHandler(.Success(currencies))
} catch {
completionHandler(.Error(error))
}
}.resume()
}
And you can run this function with the completion handlers as:
fetchRates { result in
dispatch_async(dispatch_get_main_queue()) {
switch result {
case .Success(let currencies):
for currency in currencies {
print(currency)
}
case .Error(let error):
print(error)
}
}
}
You can download the playground where you can see all this working at:
https://dl.dropboxusercontent.com/u/585261/Currency.playground.zip

How to serialize or convert Swift objects to JSON?

This below class
class User: NSManagedObject {
#NSManaged var id: Int
#NSManaged var name: String
}
Needs to be converted to
{
"id" : 98,
"name" : "Jon Doe"
}
I tried manually passing the object to a function which sets the variables into a dictionary and returns the dictionary. But I would want a better way to accomplish this.
In Swift 4, you can inherit from the Codable type.
struct Dog: Codable {
var name: String
var owner: String
}
// Encode
let dog = Dog(name: "Rex", owner: "Etgar")
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(dog)
let json = String(data: jsonData, encoding: String.Encoding.utf8)
// Decode
let jsonDecoder = JSONDecoder()
let secondDog = try jsonDecoder.decode(Dog.self, from: jsonData)
Along with Swift 4 (Foundation) now it is natively supported in both ways, JSON string to an object - an object to JSON string.
Please see Apple's documentation here JSONDecoder() and here JSONEncoder()
JSON String to Object
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let myStruct = try! decoder.decode(myStruct.self, from: jsonData)
Swift Object to JSONString
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(myStruct)
print(String(data: data, encoding: .utf8)!)
You can find all details and examples here Ultimate Guide to JSON Parsing With Swift 4
UPDATE: Codable protocol introduced in Swift 4 should be sufficient for most of the JSON parsing cases. Below answer is for people who are stuck in previous versions of Swift and for legacy reasons
EVReflection :
This works of reflection principle. This takes less code and also supports NSDictionary, NSCoding, Printable, Hashable and Equatable
Example:
class User: EVObject { # extend EVObject method for the class
var id: Int = 0
var name: String = ""
var friends: [User]? = []
}
# use like below
let json:String = "{\"id\": 24, \"name\": \"Bob Jefferson\", \"friends\": [{\"id\": 29, \"name\": \"Jen Jackson\"}]}"
let user = User(json: json)
ObjectMapper :
Another way is by using ObjectMapper. This gives more control but also takes a lot more code.
Example:
class User: Mappable { # extend Mappable method for the class
var id: Int?
var name: String?
required init?(_ map: Map) {
}
func mapping(map: Map) { # write mapping code
name <- map["name"]
id <- map["id"]
}
}
# use like below
let json:String = "{\"id\": 24, \"name\": \"Bob Jefferson\", \"friends\": [{\"id\": 29, \"name\": \"Jen Jackson\"}]}"
let user = Mapper<User>().map(json)
I worked a bit on a smaller solution that doesn't require inheritance. But it hasn't been tested much. It's pretty ugly atm.
https://github.com/peheje/JsonSerializerSwift
You can pass it into a playground to test it. E.g. following class structure:
//Test nonsense data
class Nutrient {
var name = "VitaminD"
var amountUg = 4.2
var intArray = [1, 5, 9]
var stringArray = ["nutrients", "are", "important"]
}
class Fruit {
var name: String = "Apple"
var color: String? = nil
var weight: Double = 2.1
var diameter: Float = 4.3
var radius: Double? = nil
var isDelicious: Bool = true
var isRound: Bool? = nil
var nullString: String? = nil
var date = NSDate()
var optionalIntArray: Array<Int?> = [1, 5, 3, 4, nil, 6]
var doubleArray: Array<Double?> = [nil, 2.2, 3.3, 4.4]
var stringArray: Array<String> = ["one", "two", "three", "four"]
var optionalArray: Array<Int> = [2, 4, 1]
var nutrient = Nutrient()
}
var fruit = Fruit()
var json = JSONSerializer.toJson(fruit)
print(json)
prints
{"name": "Apple", "color": null, "weight": 2.1, "diameter": 4.3, "radius": null, "isDelicious": true, "isRound": null, "nullString": null, "date": "2015-06-19 22:39:20 +0000", "optionalIntArray": [1, 5, 3, 4, null, 6], "doubleArray": [null, 2.2, 3.3, 4.4], "stringArray": ["one", "two", "three", "four"], "optionalArray": [2, 4, 1], "nutrient": {"name": "VitaminD", "amountUg": 4.2, "intArray": [1, 5, 9], "stringArray": ["nutrients", "are", "important"]}}
This is not a perfect/automatic solution but I believe this is the idiomatic and native way to do such. This way you don't need any libraries or such.
Create an protocol such as:
/// A generic protocol for creating objects which can be converted to JSON
protocol JSONSerializable {
private var dict: [String: Any] { get }
}
extension JSONSerializable {
/// Converts a JSONSerializable conforming class to a JSON object.
func json() rethrows -> Data {
try JSONSerialization.data(withJSONObject: self.dict, options: nil)
}
}
Then implement it in your class such as:
class User: JSONSerializable {
var id: Int
var name: String
var dict { return ["id": self.id, "name": self.name] }
}
Now:
let user = User(...)
let json = user.json()
Note: if you want json as a string, it is very simply to convert to a string: String(data: json, encoding .utf8)
Some of the above answers are completely fine, but I added an extension here, just to make it much more readable and usable.
extension Encodable {
var convertToString: String? {
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
do {
let jsonData = try jsonEncoder.encode(self)
return String(data: jsonData, encoding: .utf8)
} catch {
return nil
}
}
}
struct User: Codable {
var id: Int
var name: String
}
let user = User(id: 1, name: "name")
print(user.convertToString!)
//This will print like the following:
{
"id" : 1,
"name" : "name"
}
Not sure if lib/framework exists, but if you would like to do it automatically and you would like to avoid manual labour :-) stick with MirrorType ...
class U {
var id: Int
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
extension U {
func JSONDictionary() -> Dictionary<String, Any> {
var dict = Dictionary<String, Any>()
let mirror = reflect(self)
var i: Int
for i = 0 ; i < mirror.count ; i++ {
let (childName, childMirror) = mirror[i]
// Just an example how to check type
if childMirror.valueType is String.Type {
dict[childName] = childMirror.value
} else if childMirror.valueType is Int.Type {
// Convert to NSNumber for example
dict[childName] = childMirror.value
}
}
return dict
}
}
Take it as a rough example, lacks proper conversion support, lacks recursion, ... It's just MirrorType demonstration ...
P.S. Here it's done in U, but you're going to enhance NSManagedObject and then you'll be able to convert all NSManagedObject subclasses. No need to implement this in all subclasses/managed objects.
struct User:Codable{
var id:String?
var name:String?
init(_ id:String,_ name:String){
self.id = id
self.name = name
}
}
Now just make your object like this
let user = User("1","pawan")
do{
let userJson = try JSONEncoder().encode(parentMessage)
}catch{
fatalError("Unable To Convert in Json")
}
Then reconvert from json to Object
let jsonDecoder = JSONDecoder()
do{
let convertedUser = try jsonDecoder.decode(User.self, from: userJson.data(using: .utf8)!)
}catch{
}
2021 | SWIFT 5.1 | Results solution
Input data:
struct ConfigCreds: Codable {
// some params
}
usage:
// get JSON from Object
configCreds
.asJson()
.onSuccess{ varToSaveJson = $0 }
.onFailure{ _ in // any failure code }
// get object of type "ConfigCreds" from JSON
someJsonString
.decodeFromJson(type: ConfigCreds.self)
.onSuccess { configCreds = $0 }
.onFailure{ _ in // any failure code }
Back code:
#available(macOS 10.15, *)
public extension Encodable {
func asJson() -> Result<String, Error>{
JSONEncoder()
.try(self)
.flatMap{ $0.asString() }
}
}
public extension String {
func decodeFromJson<T>(type: T.Type) -> Result<T, Error> where T: Decodable {
self.asData()
.flatMap { JSONDecoder().try(type, from: $0) }
}
}
///////////////////////////////
/// HELPERS
//////////////////////////////
#available(macOS 10.15, *)
fileprivate extension JSONEncoder {
func `try`<T : Encodable>(_ value: T) -> Result<Output, Error> {
do {
return .success(try self.encode(value))
} catch {
return .failure(error)
}
}
}
fileprivate extension JSONDecoder {
func `try`<T: Decodable>(_ t: T.Type, from data: Data) -> Result<T,Error> {
do {
return .success(try self.decode(t, from: data))
} catch {
return .failure(error)
}
}
}
fileprivate extension String {
func asData() -> Result<Data, Error> {
if let data = self.data(using: .utf8) {
return .success(data)
} else {
return .failure(WTF("can't convert string to data: \(self)"))
}
}
}
fileprivate extension Data {
func asString() -> Result<String, Error> {
if let str = String(data: self, encoding: .utf8) {
return .success(str)
} else {
return .failure(WTF("can't convert Data to string"))
}
}
}
fileprivate func WTF(_ msg: String, code: Int = 0) -> Error {
NSError(code: code, message: msg)
}

Best way to convert JSON or other untyped data into typed classes in Swift?

I'm trying to parse out JSON into typed classes for safety/convenience, but it's proving very clunky. I wasn't able to find a library or even a post for Swift (Jastor is as close as I got). Here's a fabricated little snippet to illustrate:
// From NSJSONSerialization or similar and casted to an appropriate toplevel type (e.g. Dictionary).
var parsedJson: Dictionary<String, AnyObject> = [ "int" : 1, "nested" : [ "bool" : true ] ]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(fromParsedJson json: NSDictionary) {
if let parsedStringValue = json["string"] as? String {
self.stringValueWithDefault = parsedStringValue
}
if let parsedIntValue = json["int"] as? Int {
self.intValueRequired = parsedIntValue
} else {
// Raise an exception...?
}
// Optional-chaining is actually pretty nice for this; it keeps the blocks from nesting absurdly.
if let parsedBool = json["nested"]?["bool"] as? Bool {
self.nestedBoolBroughtToTopLevel = parsedBool
}
if let parsedFirstInt = json["firstInt"] as? Int {
if let parsedSecondInt = json["secondInt"] as? Int {
self.combinedIntRequired = parsedFirstInt * parsedSecondInt
}
}
// Most succinct way to error if we weren't able to construct self.combinedIntRequired?
}
}
TypedObject(fromParsedJson: parsedJson)
There's a number of issues here that I'm hoping to work around:
It's extremely verbose, since I need to wrap every single property in a copy-pasted if-let for safety.
I'm not sure how to communicate errors when required properties are missing (as noted above). Swift seems to prefer (?) using exceptions for show-stopping problems (rather than pedestrian malformed data as here).
I don't know a nice way to deal with properties that exist but are the wrong type (given that the as? casting will fail and simply skip the block, it's not very informative to the user).
If I want to translate a few properties into a single one, I need to nest the let blocks proportional to the number of properties I'm combining. (This is probably more generally a problem with combining multiple optionals into one value safely).
In general, I'm writing imperative parsing logic when I feel like I ought to be able to do something a little more declarative (either with some stated JSON schema or at least inferring the schema from the class definition).
I do this using the Jastor framework:
1) Implement a Protocol that has a single function that returns an NSDictionary response:
protocol APIProtocol {
func didReceiveResponse(results: NSDictionary)
}
2) Create an API class that defines an NSURLConnection object that can be used as a Request URL for iOS's networking API. This class is created to simply return a payload from the itunes.apple.com API.
class API: NSObject {
var data: NSMutableData = NSMutableData()
var delegate: APIProtocol?
func searchItunesFor(searchTerm: String) {
// Clean up the search terms by replacing spaces with +
var itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+",
options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
var escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music"
var url: NSURL = NSURL(string: urlPath)
var request: NSURLRequest = NSURLRequest(URL: url)
var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)
println("Search iTunes API at URL \(url)")
connection.start()
}
// NSURLConnection Connection failed.
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
println("Failed with error:\(error.localizedDescription)")
}
// New request so we need to clear the data object.
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
self.data = NSMutableData()
}
// Append incoming data.
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
self.data.appendData(data)
}
// NSURLConnection delegate function.
func connectionDidFinishLoading(connection: NSURLConnection!) {
// Finished receiving data and convert it to a JSON object.
var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
delegate?.didReceiveResponse(jsonResult)
}
}
3) Create a class with associated properties that inherits from Jastor
NSDictionary response:
{
"resultCount" : 50,
"results" : [
{
"collectionExplicitness" : "notExplicit",
"discCount" : 1,
"artworkUrl60" : "http:\/\/a4.mzstatic.com\/us\/r30\/Features\/2a\/b7\/da\/dj.kkirmfzh.60x60-50.jpg",
"collectionCensoredName" : "Changes in Latitudes, Changes in Attitudes (Ultmate Master Disk Gold CD Reissue)"
}
]
}
Music.swift
class Music : Jastor {
var resultCount: NSNumber = 0
}
4) Then in your ViewController be sure to set the delegate to self and then make a call to the API's searchITunesFor() method.
var api: API = API()
override func viewDidLoad() {
api.delegate = self;
api.searchItunesFor("Led Zeppelin")
}
5) Implement the Delegate method for didReceiveResponse(). Jastor extends your class to set a NSDictionary of the results returned from the iTunes API.
// #pragma - API Delegates
func didReceiveResponse(results: NSDictionary) {
let music = Music(dictionary: results)
println(music)
}
Short version: Since init isn't allowed to fail, validation has to happen outside of it. Optionals seem to be the intended tool for flow control in these cases. My solution is to use a factory method that returns an optional of the class, and use option chaining inside it to extract and validate the fields.
Note also that Int and Bool aren't children of AnyObject; data coming from an NSDictionary will have them stored as NSNumbers, which can't be cast directly to Swift types. Thus the calls to .integerValue and .boolValue.
Long version:
// Start with NSDictionary since that's what NSJSONSerialization will give us
var invalidJson: NSDictionary = [ "int" : 1, "nested" : [ "bool" : true ] ]
var validJson: NSDictionary = [
"int" : 1,
"nested" : [ "bool" : true ],
"firstInt" : 3,
"secondInt" : 5
]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(intValue: Int, combinedInt: Int, stringValue: String?, nestedBool: Bool?) {
self.intValueRequired = intValue
self.combinedIntRequired = combinedInt
// Use Optionals for the non-required parameters so
// we know whether to leave the default values in place
if let s = stringValue {
self.stringValueWithDefault = s
}
if let n = nestedBool {
self.nestedBoolBroughtToTopLevel = n
}
}
class func createFromDictionary(json: Dictionary<String, AnyObject>) -> TypedObject? {
// Validate required fields
var intValue: Int
if let x = (json["int"]? as? NSNumber)?.integerValue {
intValue = x
} else {
return nil
}
var combinedInt: Int
let firstInt = (json["firstInt"]? as? NSNumber)?.integerValue
let secondInt = (json["secondInt"]? as? NSNumber)?.integerValue
switch (firstInt, secondInt) {
case (.Some(let first), .Some(let second)):
combinedInt = first * second
default:
return nil
}
// Extract optional fields
// For some reason the compiler didn't like casting from AnyObject to String directly
let stringValue = json["string"]? as? NSString as? String
let nestedBool = (json["nested"]?["bool"]? as? NSNumber)?.boolValue
return TypedObject(intValue: intValue, combinedInt: combinedInt, stringValue: stringValue, nestedBool: nestedBool)
}
class func createFromDictionary(json: NSDictionary) -> TypedObject? {
// Manually doing this cast since it works, and the only thing Apple's docs
// currently say about bridging Cocoa and Dictionaries is "Information forthcoming"
return TypedObject.createFromDictionary(json as Dictionary<String, AnyObject>)
}
}
TypedObject.createFromDictionary(invalidJson) // nil
TypedObject.createFromDictionary(validJson) // it works!
I've also done the following to convert to/from:
class Image {
var _id = String()
var title = String()
var subTitle = String()
var imageId = String()
func toDictionary(dict dictionary: NSDictionary) {
self._id = dictionary["_id"] as String
self.title = dictionary["title"] as String
self.subTitle = dictionary["subTitle"] as String
self.imageId = dictionary["imageId"] as String
}
func safeSet(d: NSMutableDictionary, k: String, v: String) {
if (v != nil) {
d[k] = v
}
}
func toDictionary() -> NSDictionary {
let jsonable = NSMutableDictionary()
self.safeSet(jsonable, k: "title", v: self.title);
self.safeSet(jsonable, k: "subTitle", v: self.subTitle);
self.safeSet(jsonable, k: "imageId", v: self.imageId);
return jsonable
}
}
Then I simply do the following:
// data (from service)
let responseArray = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: nil) as NSArray
self.objects = NSMutableArray()
for item: AnyObject in responseArray {
var image = Image()
image.toDictionary(dict: item as NSDictionary)
self.objects.addObject(image)
}
If you want to POST the data:
var image = Image()
image.title = "title"
image.subTitle = "subTitle"
image.imageId = "imageId"
let data = NSJSONSerialization.dataWithJSONObject(image.toDictionary(), options: .PrettyPrinted, error: nil) as NSData
// data (to service)
request.HTTPBody = data;