Dealing with JSON is one of those tasks that almost every developer has to perform in order to construct data models with which to populate project views. However, building and testing a good data model can be time-consuming and prone to mistakes.
JSON Parsing in Swift Before Codable
Prior to Swift 4, Swift's Foundation library provided native JSON parsing by way of the JSONSerialization API. Simply put, JSONSerialization would convert an API payload into a collection of dictionaries with nested arrays. However, though converting the JSON payload to a Swift data structure was relatively painless, accessing the nested data was anything but.
For example:
let stringToReturn = """
{
"villains":{
"Darth Vader":{
"allegiance":"Dark Side",
"ice cream":"chocolate"
},
"The Emperor":{
"allegiance":"Dark Side",
"ice cream":"strawberry"
},
"Boba Fett":{
"allegiance":"Dark Side",
"ice cream":"rum-and-raisin"
}
}
}
"""
let jsonPayload = stringToReturn.data(using: String.Encoding.utf8, allowLossyConversion: false)!
do {
let json = try JSONSerialization.jsonObject(with: jsonPayload, options: [JSONSerialization.ReadingOptions.mutableContainers]) as! [String: AnyObject]
if let villains = json["villains"] {
if let darthVader = villains["Darth Vader"] as? [String: String] {
if let iceCreamFlavor = darthVader["ice cream"] {
print("Darth Vader considers \(iceCreamFlavor) ice cream as the one true ice cream.")
}
}
}
} catch let error as Error {
print ("We encountered an error: \(error.localizedDescription)")
}
For anything more complicated than the simple example above (think nested arrays of arrays), accessing keys and values quickly grew into the ‘if let’
Pyramid of Doom.
Angelo Villegas provides a good example of the Pyramid of Doom and its attendant
rightward drift
in
this article.
Third-party frameworks, such as
SwiftyJSON,
ameliorated some of this pain, but it meant bloating your binary with a third-party framework. Using
multiple optional bindings
offered a better alternative than a cascade of nested
if let
statements, but it was still a lot of work to parse anything but the simplest of JSON payloads.
Enter Codable
In September, 2017, the Swift Core Team released Swift 4.0. Among many enhancements, the Foundation library received the new Codable protocol. To paraphrase Apple’s documentation on the topic, Codable is a protocol that allows developers to encode and decode their conforming data types to be compatible with external representations of their data such as JSON. An in-depth discussion of Codable is beyond the scope of this post, but I encourage you to have a look at Ben Scheirman’s Ultimate Guide to JSON Parsing with Swift 4 to read about the benefits Codable offers. In short, Codable significantly reduces the overhead of unpacking JSON payloads for developers and does away with the need for optional bindings.
However, even with the convenience and ease of use Codable offers, mapping a new API to a Swift model can be time-consuming. Below, I’ll show you a quick and effortless way to get your data modeling out of the way so you can get on with actually using the data in your project.
Data Modeling with Xcode Playgrounds
Xcode Playgrounds allows us to quickly build and test data models without having to set up a full Xcode project and make countless API calls to our endpoint. For our purposes, we’ll use SpaceX’s API for rocket data.
-
1. Open Xcode Playgrounds
-
2. Open your browser of choice and enter https://api.spacexdata.com/v2/rockets
-
3. Select and copy the resulting JSON
-
4. Go to jsonprettyprint.com to prettify the JSON string*
-
5. Open the side pane in Xcode Playgrounds
-
6. Right click the 'Sources' folder and select 'New File'
-
7. Name it Rockets.json
-
8. Paste the prettified JSON into this file and save
-
9. Add another new file to the 'Sources' folder and name it RocketModel.swift
-
10. For our purposes, we’ll use a struct to model our rocket data. In RocketModel.swift, paste the following**:
public typealias Rockets = [RocketModel]
public struct Height: Codable {
public let meters: Int
public let feet: Double
}
public struct RocketModel: Codable {
// IMPORTANT: The variable names here must match those in the CodingKeys enum
public let id: String
public let name: String
public let type: String
public let active: Bool
public let stages: Int
public let boosters: Int
public let costPerLaunch: Int
public let successRate: Int
public let firstFlight: String
public let country: String
public let company: String
public let height: Height
public let description: String
enum CodingKeys: String, CodingKey {
case id
case name
case type
case active
case stages
case boosters
case costPerLaunch = "cost_per_launch"
case successRate = "success_rate_pct"
case firstFlight = "first_flight"
case country
case company
case height
case description
}
}
*There are other options available (see, for example, Paw ), but this way is reasonably quick and easy.
** Pro tip: to expose the classes and types in your Sources folder to the main Playgrounds file, you have to explicitly mark their declarations as ‘public’.
Explanation
Our data will be contained in a struct called ‘RocketModel’. To use Codable, we mark our data types as ‘Codable’ when we declare them. Codable allows us to map our struct’s properties as one-to-one representations of our JSON object’s items by simply listing them out and assigning the appropriate types. For example, to include the JSON entries ‘id’, ‘name’, ‘type’, etc, we simply declare corresponding constants in our RocketModel struct:
public struct RocketModel: Codable {
public let id: String
public let name: String
public let type: String
public let active: Bool
}
If the JSON object uses key names that differ from the property names we want to use, we can tell Codable to map those key names to our preferred names by declaring an enum with
String
and
CodingKey
as its associated types. Note that each case of the enum should match the property name that we declared in our list of properties.
enum CodingKeys: String, CodingKey {
case id
case name
case type
case active
case stages
case boosters
case costPerLaunch = "cost_per_launch"
case successRate = "success_rate_pct"
case firstFlight = "first_flight"
case country
case company
case height
case description
}
For those properties that require names that differ from those in the JSON object, list their JSON name as an associated string value. For example:
case costPerLaunch = "cost_per_launch"
Here’s a list of more detailed steps to map JSON objects to Swift value types:
-
1. For JSON payloads that are unnamed arrays, decide on a name for each item in the array. For example,
Rocket
orRocketModel
-
2. Create a typealias that is the plural of the name and declare its value as an array of the type you decided on in Step 1. For example:
typealias Rockets = [RocketModel]
3. Next, list out as constants (with the appropriate types) all the dictionary entries in the JSON array in which you’re interested. Note, that if you’re only interested in a handful of entries, you can just include those; Codable will take care of the rest.
4. For entries with nested entries or containers, make a separate struct, following the pattern used above (excluding the typealias) and assign the struct as the type of one of the items in the parent container. For example, for the ‘height’ entry—
"height": {
"meters": 70,
"feet": 229.6
}
—we’ll declare a 'Height' struct than conforms to Codable and has two properties, namely “meters” and “feet”:
public struct Height: Codable {
public let meters: Int
public let feet: Double
}
5. Rinse and repeat until you have stubs for all the data in which you’re interested.
6. Assign the payload to a data object, e.g.
data
and instantiate a decoder:
guard let url = Bundle.main.url(forResource: "Rockets", withExtension: "json") else {
fatalError() // Don’t use fatalError in production code
}
guard let data = try? Data(contentsOf: url) else {
fatalError()
}
let decoder = JSONDecoder()
7. Then, decode the data object like this (making sure to handle any errors thrown during the decoding process):
do {
let response = try decoder.decode(Rockets.self, from: data)
for rocketModel in response {
print(rocketModel.name)
}
} catch {
print(error)
}
Notice that we’re passing in
Rockets.self
(the typealias that we declared in RocketModel.swift) as the model in which to place the data.
8. To access a data property in the model, loop through the array of models in the response and use dot notation:
for rocketModel in response {
print(rocketModel.name)
}
So . . . You Mentioned 5 Steps?
Although this approach is significantly easier and more convenient than JSONSerialize, it still requires a fair bit of heavy lifting, a phrase which is synonymous to ‘qualifies for automation’. Moreover, at the top of this post I promised to demonstrate generating a data model in 5 easy steps. As it happens, a web app called QuickType solves this problem with only a modicum of effort on the developer’s side.
Steps:
-
1. Head over to app.quicktype.io.
-
2. From the ‘Language’ tab in panel on the right, select ‘Swift’ from the drop-down menu.
-
3. Enter a name for your data model in the textbox on the left (‘Rockets’ in our case).
-
4. Paste your JSON payload below the model name.
-
5. Watch as QuickType generates your model for you.
Parsing a JSON payload and mapping its entries to a data model in Swift is far less of a hassle than it used to be now that Codable has entered the frame. Adding QuickType to your workflow allows you to quickly generate a template for a data model that you can finetune and prune as necessary.
Available for Download: Xcode Playgrounds file