Update 12/21/2015: Updated for Xcode 7.1 and Swift 2.0.
JavaScript Object Notation, or JSON for short, is a common way to transmit data to and from web services. It is simple to use and human-readable, which is why it’s so incredibly popular.
Consider the following JSON snippet:
[ { "person": { "name": "Dani", "age": "24" } }, { "person": { "name": "ray", "age": "70" } } ] |
In Objective-C, parsing and deserializing JSON is fairly straightforward:
NSArray *json = [NSJSONSerialization JSONObjectWithData:JSONData options:kNilOptions error:nil]; NSString *age = json[0][@"person"][@"age"]; NSLog(@"Dani's age is %@", age); |
In Swift, parsing and deserializing JSON is a little more tedious due to Swift optionals and type-safety:
var json: Array! do { json = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions()) as? Array } catch { print(error) } if let item = json[0] as? [String: AnyObject] { if let person = item["person"] as? [String: AnyObject] { if let age = person["age"] as? Int { print("Dani's age is \(age)") } } } |
In the code above, each object from the JSON must be checked before you use it via optional binding. This makes your code safe; but the more complicated your JSON is, the more ugly your code becomes.
As part of Swift 2.0 the guard
statement was introduced to help get rid of nested if
statements:
guard let item = json[0] as? [String: AnyObject], let person = item["person"] as? [String: AnyObject], let age = person["age"] as? Int else { return; } print("Dani's age is \(age)") |
Still too verbose, isn’t it? But how could you make it simpler?
In this JSON tutorial, you’re going to learn an easier way to parse JSON in Swift – using a popular open source library called Gloss.
In particular, you’ll use Gloss to parse and map a JSON document containing the top 25 apps in the US App Store. You’ll see that it can be just as easy as it is in Objective-C! :]
Getting Started
Download the starter playground for this tutorial.
Since a user interface isn’t important for this JSON tutorial, you’ll be working exclusively with playgrounds.
Open Swift.playground in Xcode and have a look around.
The starter playground file has a few source and resource files included to keep the focus purely on parsing JSON in Swift. Take a look at it’s structure to get an overview of what’s included:
-
The Resources folder bundles resources that can be accessed from your Swift code.
- topapps.json: Contains the JSON string to be parsed.
-
The Sources folder contains additional Swift source files that your main playground code can access. Putting extra supporting .swift files into the Sources folder makes it easy to keep your playground clean and readable.
- App.swift: A plain old Swift struct representing an app. Your goal is to parse the JSON into a collection of these objects.
- DataManager.swift: Manages the data retrieval whether local or from the network. You’ll use the methods in this file to load some JSON later in this tutorial.
Once you feel like you have a good understanding of what’s currently in the playground, read on!
Parsing JSON the Native Swift Way
First, you’ll start by parsing JSON the native Swift way – i.e. without using any external libraries. This will help you understand the benefit of using a library like Gloss.
Let’s parse the provided JSON file to get the name of the #1 app on the App Store!
First, while you are going to work with dictionaries a lot, define a type alias at the top of the playground:
typealias Payload = [String: AnyObject] |
Complete the code inside the callback of getTopAppsDataFromFileWithSuccess
as follows:
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in var json: Payload! // 1 do { json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? Payload } catch { print(error) XCPlaygroundPage.currentPage.finishExecution() } // 2 guard let feed = json["feed"] as? Payload, let apps = feed["entry"] as? [AnyObject], let app = apps.first as? Payload else { XCPlaygroundPage.currentPage.finishExecution() } guard let container = app["im:name"] as? Payload, let name = container["label"] as? String else { XCPlaygroundPage.currentPage.finishExecution() } guard let id = app["id"] as? Payload, let link = id["label"] as? String else { XCPlaygroundPage.currentPage.finishExecution() } // 3 let entry = App(name: name, link: link) print(entry) XCPlaygroundPage.currentPage.finishExecution() } |
Here’s what’s going on above:
- First you deserialize the data using
NSJSONSerialization
. - You need to check each and every subscript value in the JSON object to make sure that it is not
nil
. Once you’re sure it has a valid value, search for the next object. Once you’ve made your way through all of the subscripts, you’ll get thename
andlink
values to work with.Note that if any element in the JSON is unexpected, you bail on the whole mess and the app name never gets printed. This is desirable in this case.
- The last step is initializing an
App
using the values ofname
andlink
and printing it to the console.
Save and run your storyboard; you should see the following result in your debugger console:
App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")
Yes — “Game of War – Fire Age” is the #1 app in the JSON file.
It took a lot of verbose code just to retrieve the name of the first app — it’s time to see how Gloss stacks up.
Introduction to JSON object mapping
Object mapping is a technique to turn JSON objects into native Swift objects. After defining model objects and corresponding mapping rules, Gloss does the hard work by performing the parsing instead of you.
It has significant benefits over the approach you just tried:
- You code will be cleaner, reusable and easier to maintain.
- You can work with objects rather than generic arrays and dictionaries.
- You can extend model classes to add additional functionality.
Sounds good, huh? Let’s see how it works!
Parsing JSON the Gloss way
To keep everything nice and clean, create a new playground called Gloss.playground, then copy over topapps.json into Resources and DataManager.swift into Sources.
Integrating Gloss in Your Project
It’s fairly straightforward to integrate Gloss into your project/playground:
- Click the following link Gloss Repo Zip File and save the library to a convenient location.
- Unzip it and drag the Gloss-master/Gloss/Gloss folder into your Sources folder of your playground.
Your Project Navigator should look like this:
That’s it! Now that you have Gloss added to your playground, you can start parsing JSON without the optional binding headache!
Mapping JSON to objects
First, you have to define how your model objects relate to your JSON document.
Model objects must conform to the Decodeable
protocol, which enables them to be decoded from JSON. To do so, you’ll implement the init?(json: JSON)
initializer as defined in the protocol.
Have a look at the structure of topapps.json and create the data model!
TopApps
The TopApps
model represents the top level object, and it contains a single key-value pair:
{ "feed": { ... } } |
Create a new Swift file called TopApps.swift in the Sources folder of your playground and add the following code:
public struct TopApps: Decodable { // 1 public let feed: Feed? // 2 public init?(json: JSON) { feed = "feed" <~~ json } } |
- Define your model’s properties. In this case there is only one. You’ll define the
Feed
model object later. - Make sure
TopApps
conforms to theDecodable
protocol by implementing the custom initializer.
You might wondering what <~~
is! It's called the Encode Operator and it is defined in Gloss's Operators.swift file. Basically it tells Gloss to grab the value which belongs to the key feed
and encode
it. Feed
is a Decodable
object as well; so Gloss will delegate the responsibility of the encoding to this class.
Feed
The Feed
object is very similar to the top level object. Feed
s have two key-value pairs, but since you are only interested in the top 25 apps, it's not necessary to process the author
object.
{ "author": { ... }, "entry": [ ... ] } |
Create a new Swift file called Feed.swift in the Sources folder of your playground and define Feed
as follows:
public struct Feed: Decodable { public let entries: [App]? public init?(json: JSON) { entries = "entry" <~~ json } } |
App
App
is the last model object you are going to define. It represents an app in the chart:
{ "im:name": { "label": "Game of War - Fire Age" }, "id": { "label": "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2", ... }, ... } |
Create a new Swift file called App.swift in the Sources folder of your playground and add the following code:
public struct App: Decodable { // 1 public let name: String public let link: String public init?(json: JSON) { // 2 guard let container: JSON = "im:name" <~~ json, let id: JSON = "id" <~~ json else { return nil } guard let name: String = "label" <~~ container, link: String = "label" <~~ id else { return nil } self.name = name self.link = link } } |
Feed
andTopApp
both used optional properties. It is possible to define a property as non-optional if you are sure that the JSON being used will always contain the values to populate them.- You don't necessarily have to create model objects for every member in the JSON. For example, in this case it makes no sense to create a model for in:name and id. When working with non-optional and nested objects, always make sure to check for
nil
.
Now that your model classes are ready the only thing left is to let Gloss do it's job! :]
Open the playground file and add the following code:
import UIKit import XCPlayground XCPlaygroundPage.currentPage.needsIndefiniteExecution = true DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in var json: [String: AnyObject]! // 1 do { json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? [String: AnyObject] } catch { print(error) XCPlaygroundPage.currentPage.finishExecution() } // 2 guard let topApps = TopApps(json: json) else { print("Error initializing object") XCPlaygroundPage.currentPage.finishExecution() } // 3 guard let firstItem = topApps.feed?.entries?.first else { print("No such item") XCPlaygroundPage.currentPage.finishExecution() } // 4 print(firstItem) XCPlaygroundPage.currentPage.finishExecution() } |
- First you deserialize the data using
NSJSONSerialization
. Same way you did before. - Initialize an instance of
TopApps
by feeding the JSON data into it's constructor. - Get the #1 app by simply getting the first entry of the feed.
- Print the app to the console.
Seriously — that's all the code you need.
Save and run your storyboard; you'll see that you successfully get the app name again, but this time in a more elegant way:
App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")
That takes care of parsing local data — but how would you parse data from a remote source?
Retrieving Remote JSON
It's time to make this project a bit more realistic. Normally, you'd be retrieving remote data, not data from a local file. You can easily grab the App Store ratings using an online request.
Open DataManager.swift and define the Top Apps URL:
let TopAppURL = "https://itunes.apple.com/us/rss/topgrossingipadapplications/limit=25/json" |
then add the following method to DataManager
's implementation:
public class func getTopAppsDataFromItunesWithSuccess(success: ((iTunesData: NSData!) -> Void)) { //1 loadDataFromURL(NSURL(string: TopAppURL)!, completion:{(data, error) -> Void in //2 if let data = data { //3 success(iTunesData: data) } }) } |
The above code looks pretty familiar; but instead of retrieving a local file you're using NSURLSession
to pull the data from iTunes. Here's what's happening in detail:
- First you call
loadDataFromURL
; this takes the URL and a completion closure that passes in anNSData
object. - Next you make sure the data exists using optional binding.
- Finally, you pass the data to the success closure as you did before.
Open your storyboard and replace the line
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in |
with
DataManager.getTopAppsDataFromItunesWithSuccess { (data) -> Void in |
Now you're retrieving real data from iTunes.
Save and run your storyboard; you'll see that the data parsing still arrives at the same conclusion of the #1 app:
App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")
The above value may be different for you, as the top apps in the App Store change constantly.
Usually people aren't just interested in the top app in the App Store — they want to see a list of all top apps. You don't have to code anything in order to access them - you can easily access them with the following code snippet:
topApps.feed?.entries |
Peeking Under the Hood at Gloss
You can definitely see that Gloss works really well at taking care of the parsing process — but how does it work under the hood?
<~~
is a custom operator for a set of Decoder.decode
functions. Gloss has built-in support for decoding a lot of types:
- Simple types (
Decoder.decode
) - Decodable models (
Decoder.decodeDecodable
) - Simple arrays (
Decoder.decode
) - Arrays of Decodable models (
Decoder.decodeDecodableArray
) - Enum types (
Decoder.decodeEnum
) - Enum arrays (
Decoder.decodeEnumArray
) - NSURL types (
Decoder.decodeURL
) - NSURL arrays (
Decode.decodeURLArray
)
In this tutorial you relied heavily on Decodable
models. If you need something more complex, it's possible to extend Decoder
and provide your own decoding implementation.
Of course Gloss can convert objects back to JSON as well. If you are interested in doing that, have a look at the Encodable
protocol.
Where to Go From Here?
Here is the final playground from the above JSON tutorial.
You can definitely use the playground as starter code for your next app; simply replace the URL for retrieving remote data with your own URL, handle the individual keys and indices of your new JSON response, and you'll have a new project that parses something else like the results of football matches or any other piece of data that can be downloaded over the network as JSON.
Development on Gloss will continue here on Github, so keep an eye out for the latest updates.
Swift is still evolving as well, so you'll want to keep up to date on Apple's documentation for any future updates for the language.
I hope you enjoyed this JSON tutorial; don't forget to check out all the other Swift tutorials on the website. If you have any questions or comments, feel free to join the discussion below!
The post Swift Tutorial: Working with JSON appeared first on Ray Wenderlich.