Recently I started a new iOS Swift project and spent way more time than I would like trying to find a JSON parser that could handle the various JSON data models I was working with. In this post I will document some real code samples that should prove useful for other iOS developers looking to get off to head start with data modelling in Swift.
Handling JSON is a very common task with modern app development whether its consuming some REST Service API, loading a JSON file or document objects from database. With regards to Windows C# apps Newtonsoft JSON is the popular choice and similarly with Java for Android there is GSON. But what library to use for iOS apps? Previously I had used libraries like JSONModel to parse JSON data into native objects and it worked pretty well. But the iOS developer landscape has changed with the shift from Objective C to Swift so I wanted to find a Swift based framework. There are a number of open source Swift JSON parsers, but the ones I tried resulted in code mountains just to parse some format of JSON. This felt like a fail compared to the elegant manner of Newtonsoft or GSON object models. I was surprised how hard it was to pinpoint the one Swift library that could satisfy all my parsing needs. But with Argo I feel I’ve discovered the golden JSON parsing library for iOS Swift development.
I’m a long time user of CocoaPods for Xcode source control projects as it makes it easier to avoid jamming up a repro with binaries. However the precompiled versions on CocoaPods don’t always provide the latest version available on GitHub. This is where Carthage comes in as you specifically request a tag version or branch on GitHub. Carthage can be quickly installed using Homebrew as mentioned in the [installing Carthage docs].(https://github.com/Carthage/Carthage#installing-carthage)
brew install carthage
To setup create a new text file and save it as ‘Cartfile’ inside your Xcode project folder. (In this case I’m requesting a specific version of Argo and Curry for use with Swift 2)
github "thoughtbot/Argo" == 3.0.2
github "thoughtbot/Curry" == 2.2
Once you have installed Carthage and saved a ‘Cartfile’ then you need to build the frameworks.
carthage update
to build frameworks for all platforms. NB: For packages that can only be built for a single platform use carthage build --platform iOS
carthage copy-frameworks
and add Input Files path to ‘*.framework’Argo decodes standard property types (String, Int, UInt, Int64, UInt64, Double, Float, Bool
) as well as arrays and optional properties. You can decode a nested object or an array of nested objects that conform to the ‘Decodable’ protocol. In fact you can even do inception - using the same struct within itself as shown below. One thing that might require explanation is Argo’s sugar syntax. The summary of the sugar syntax is this:
<^>
syntax pulls the first property, and <*>
pulls subsequent properties.<|
syntax relates to a property.<|?
syntax relates to an optional property.<||
syntax relates to an array of ‘decodable’ objects.<||?
syntax relates to an array of optional ‘decodable’ objectsimport Foundation
import Argo
import Curry
struct SomeModel {
let id : String
let name : String
let total : Int
let isHighlighted : Bool
let optional : String?
let children : [SomeModel]?
}
extension SomeModel: Decodable {
static func decode(j: JSON) -> Decoded<SomeModel> {
return curry(SomeModel.init)
<^> j <| "_id"
<*> j <| "name"
<*> j <| "total"
<*> j <| "highlighted"
<*> j <|? "optional"
<*> j <||? "children"
}
}
It can be advantageous to parse URL and date values as native types instead of String types. To get this to work with Argo you need to make a parser which wraps NSURL and NSDate in the ‘Decoded’ type. But first I made a Uri helper to encode url strings as NSURL and a Date helper to convert a date string (of a known format) to NSDate.
import Foundation
class Uri {
static func encodeURLString(urlString: String) -> String {
let characterSet = NSMutableCharacterSet()
characterSet.formUnionWithCharacterSet(NSCharacterSet.URLPathAllowedCharacterSet())
characterSet.formUnionWithCharacterSet(NSCharacterSet.URLQueryAllowedCharacterSet())
return urlString.stringByAddingPercentEncodingWithAllowedCharacters( characterSet ) ?? urlString
}
}
import Foundation
enum DateFormats : String {
case Milliseconds = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"
case Seconds = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
}
class Date {
static private let dateFormatter = NSDateFormatter()
// converts String (using array of potential date formats) to Date
static func StringToDate(dateString : String) -> NSDate? {
var date : NSDate? = nil
let dateFormats : [String] = [DateFormats.Milliseconds.rawValue, DateFormats.Seconds.rawValue]
for dateFormat in dateFormats {
dateFormatter.dateFormat = dateFormat
if let formatedDate = dateFormatter.dateFromString(dateString) {
date = formatedDate
break
}
}
return date
}
}
The Parser helper returns objects wrapped in Decoded type:
import Foundation
import Argo
class Parser {
static func toNSURL(urlString : String) -> Decoded<NSURL> {
let urlEncodedString = Uri.encodeURLString(urlString)
guard let url = NSURL(string: urlEncodedString) else {
return Decoded.Failure(DecodeError.Custom("Failed to parse String to NSURL"))
}
// Return NSURL wrapped in .Success
return pure(url)
}
static func toNSDate(dateString : String) -> Decoded<NSDate> {
guard let date = Date.StringToDate(dateString) else {
return Decoded.Failure(DecodeError.Custom("Failed to parse String to NSDate"))
}
// Return NSDate wrapped in .Success
return pure(date)
}
// optional (nil values are allowed)
static func toOptionalNSDate(dateString : String?) -> Decoded<NSDate?> {
guard let str = dateString else {
return pure(nil) // No date string
}
guard let date = Date.StringToDate(str) else {
return Decoded.Failure(DecodeError.Custom("Failed to parse String to NSDate"))
}
// Return NSDate wrapped in .Success
return pure(date)
}
}
Example model with NSURL and NSDate using the Parser helper (note the extra brackets):
import Foundation
import Argo
import Curry
struct SomeModel {
let id : String
let url : NSURL
let dateCreated : NSDate?
}
extension SomeModel: Decodable {
static func decode(j: JSON) -> Decoded<SomeModel> {
return curry(SomeModel.init)
<^> j <| "_id"
<*> (j <| "url" >>- Parser.toNSURL)
<*> (j <|? "date_created" >>- Parser.toOptionalNSDate)
}
}
Often the first thing I like to do is to load a JSON file to configure my app. For example you might have various JSON config files for localhost, staging and production settings.
{
"app_url": "https://someapp.azurewebsites.net",
}
The data model using Argo & Curry would look like this in Swift:
import Foundation
import Argo
import Curry
struct ConfigModel {
let appUrl: String
}
extension ConfigModel: Decodable {
static func decode(j: JSON) -> Decoded<ConfigModel> {
return curry(ConfigModel.init)
<^> j <| "app_url"
}
}
To load the JSON file within the app bundle I use a file helper:
// returns json from file
static func loadJSON(file: String) -> AnyObject? {
let path : String? = NSBundle.mainBundle().pathForResource(file, ofType: "json")
guard let unwrappedPath = path else {
return nil
}
let fileContents : NSData? = NSData(contentsOfFile: unwrappedPath)
guard let data = fileContents else {
return nil
}
do {
return try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
} catch let error as NSError {
print(error.localizedDescription)
}
return nil
}
The loaded JSON can be parsed into the ‘ConfigModel’ using Argo’s decode method.
func loadConfig(file:String) -> ConfigModel? {
let json : AnyObject? = loadJSON(file)
if let j = json {
return decode(j)
}
debugPrint("Error with \(file).json file")
return nil
}
While this is fine for converting one type of object, what if you have multiple data models? You could quickly end up with a lot of repetitive code. One of the powerful things with Swift 2 is that it supports Abstract Types. Argo needs a little help to ensure the abstract type conforms to the Decodable type so there is slightly more boilerplate in this case, but it should help keep things DRY.
func loadJSONFile<T: Decodable where T == T.DecodedType>(file : String) -> T? {
let json : AnyObject? = loadJSON(file)
if let j: AnyObject = json {
return decode(j)
}
debugPrint("Error with \(file).json file")
return nil
}
The JSON config file can be loaded in AppDelegate in the ‘didFinishLaunchingWithOptions’ method:
var config: ConfigModel?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
config = loadJSONFile("config")
return true
}
I also needed to parse various JSON results provided by via REST service API. To handle the REST request here I’ll be using the Alamofire library for Swift. Alamofire can also be added to the Cartfile:
github "Alamofire/Alamofire" ~\> 3.4
Below is an example snippet taken from a login POST request. When using Alamofire the JSON data is available as response.result.value
which can be parsed with the Argo decode method.
func login(username: String, password: String) {
let authURL : NSURL = NSURL(string: "https://some_auth_endpoint")
// Request body params
let parameters : [String: AnyObject] = [
"username": username,
"password": password
]
// Initiate async request using Alamofire
Alamofire.request(.POST, authURL, parameters: parameters, encoding: .JSON).responseJSON {
response in
// Return early on failure
guard response.response?.statusCode == 200 else {
let alert = UIAlertController.init(title: "Error", message: "Failed to login, please check username and password.", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "Ok", style: .Cancel, handler: {(alertAction: UIAlertAction) in
alert.dismissViewControllerAnimated(true, completion: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
return
}
// Parse JSON result value using Argo
guard let result = response.result.value,
let authToken: AuthTokenModel = decode(result) else {
debugPrint("Auth token model parse error")
return
}
// Login was successful, do stuff here and then navigate to home screen...
}
}
One thing to point out: I have used very simple parse error detection here - it either decodes or it doesn’t and there is no indication of what went wrong during the decode process. With smaller data models this form of indication is perfectly adequate. But when you are working with complex data models then this type of error reporting is not granular enough to pinpoint the exact the problem if you get a parse error. Fortunately Argo provides a way to parse with failure reporting by using a Decoded type.
// Get JSON data from Alamofire response
guard let result = response.result.value else {
print("No request result")
return
}
// Try decoding model with failure reporting by using Argo's Decoded type
let decodeResult: Decoded<SomeModel> = decode(result)
switch(decodeResult) {
case .Failure:
print("Failed to decode model: \(decodeResult.error?.description)")
return
case .Success:
print("Decode success")
}
// Assign decoded value to data model
guard let report : SomeModel = decodeResult.value else {
print("Error unwrapping Report result")
return
}
I found this an absolutely invaluable technique to be able to debug issues with my complex models, especially as models are pretty verbose and its always hard to spot that one string mistake.
What about storing loaded data for offline use? JSON documents can be stored with revisions using a Couchbase Lite database. The problem here is Argo only accommodates decode, but the native objects will need encoded back into JSON for use with Couchbase. This is where Ogra (Argo in reverse) comes in. The only thing is you will need to extend the data object with an encode method. If you found this post useful or if you would be interested to see some Ogra to Couch examples just fire me a tweet @deadlyfingers.