Two or three weeks into my iOS bootcamp, I saw something that looked like this...
{
"latitude": 37.8267,
"longitude": -122.4233,
"timezone": "America/Los_Angeles",
"currently": {
"time": 1557685055,
"summary": "Mostly Cloudy",
"icon": "partly-cloudy-day",
"nearestStormDistance": 0,
"precipIntensity": 0,
"precipProbability": 0,
"temperature": 53.72,
"apparentTemperature": 53.72,
"dewPoint": 50.31,
"humidity": 0.88,
"pressure": 1016.37,
"windSpeed": 6.11,
"windGust": 10.03,
"windBearing": 260,
"cloudCover": 0.9,
"uvIndex": 4,
"visibility": 10,
"ozone": 351.5
}
}
~~~{% endraw %}
I remember how magical it felt when I was taught how to parse web data (JSON) and use it in meaningful objects in my code. Hundreds of lines of data, parsed down into usually 5-7 pieces of useful data relevant to what I was doing. ย The first time was some sample JSON that our instructor had given us, the second time was creating JSON of our own and grouping it together. Finally, we started getting JSON from the web. DarkSkys, Spotify, Google APIs, we were consuming their data and making it useful to our own needs. Thousands of pieces of data bending their will to my fingertips--obey me puny pieces of binary!
The process we were taught was to create a model with the relevant data you want, then write some code to take the data that comes in from a network call, turn it into a dictionary, and extract the values from the necessary keys. Then inject that data into your model instance. My very first attempt looked like this...
##### Step One: Create the model
I wanted a few things to show. Weather summary, temperature, chance of rain, basically every piece of data a farmer with crops in the field might find relevant. (I'm from the South. Weather is ALWAYS a topic of conversation!). I was shown a neat trick where I could create a custom initializer that accepted a dictionary and parse the data right there in the model. I was indeed feeling good about my skills.{% raw %}
~~~swift
class WeatherData: NSObject {
var latitude: Double = 0.0
var longitude: Double = 0.0
var summaryDescription : String = ""
var icon : String = ""
var precipProbability : Double = 0.0
var temperature : Double = 0.0
var humidity : Double = 0.0
var windSpeed : Double = 0.0
override init() {
super.init()
self.latitude = 0.0
self.longitude = 0.0
self.summaryDescription = ""
self.icon = ""
self.precipProbability = 0.0
self.temperature = 0.0
self.humidity = 0.0
self.windSpeed = 0.0
}
init(dict: [String: AnyObject]) {
super.init()
if let latitude = dict["latitude"] as? Double {
self.latitude = latitude
}
if let longitude = dict["longitude"] as? Double {
self.latitude = latitude
}
if let currentlyDict = dict["currently"] as? [String: AnyObject] {
print(currentlyDict)
if let summary = currentlyDict["summary"] as? String {
self.summaryDescription = summary
} else {
print("I could not parse summary")
}
if let icon = currentlyDict["icon"] as? String {
self.icon = icon
} else {
print("I could not parse the icon")
}
if let precipitation = currentlyDict["precipProbability"] as? Double {
self.precipProbability = precipitation
} else {
print("I could not parse precipitation")
}
if let temp = currentlyDict["temperature"] as? Double {
self.temperature = temp
} else {
print("I could not parse the temperature")
}
if let humid = currentlyDict["humidity"] as? Double {
self.humidity = humid
} else {
print("I could not parse humidity")
}
if let wind = currentlyDict["windSpeed"] as? Double {
self.windSpeed = wind
} else {
print("I could not parse speed")
}
}
}
}
~~~{% endraw %}
#####Step Two: Take data and turn it into a dictionary
I'd then make a network call, and if all went according to plan, would take raw data and process it with a handle little function...{% raw %}
~~~swift
func parseJSON(_ data: Data?) -> [String: AnyObject]? {
var theDictionary : [String: AnyObject]? = nil
if let data = data {
do {
if let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject] {
theDictionary = jsonDictionary
//print(jsonDictionary)
} else {
print("Could not parse jsonDictionary")
}
} catch {
}
} else {
print("Could not unwrap data")
}
return theDictionary
}
~~~{% endraw %}
#####Step Three: Call for the data, parse, and inject into the model
Finally, somewhere in my completion handler to the network call, I wrote some code where I take the raw data that comes back, use my handy {% raw %}`parseJSON`{% endraw %} to serialize it and turn it into a dictionary, and then inject it into my {% raw %}`WeatherData`{% endraw %} model using my custom initializer.{% raw %}
~~~swift
// ...somehwere in the network call...
if let jsonDictionary = self.parseJSON(data) {
print(jsonDictionary)
let theWeatherData = WeatherData(dict: jsonDictionary)
DispatchQueue.main.async(execute: {
self.delegate?.passWeather(theWeatherData)
})
print(theWeatherData)
}
} else {
print("I could not parse the dictionary")
}
~~~{% endraw %}
The process got pretty involved pretty quickly. If you had an array or dictionary of information and within that more arrays of dictionaries, the parsing strategy got incredibly complex. It wasn't uncommon for me to write down on paper how I needed to get into deeper levels of nested information. It reminds me of the old Atari game "Dig Dug".
When Codable came out a couple years later, I was impressed but never had much need to use it. Scratch that. I was too lazy to bother learning. At the time, my job didn't require much pulling down of data, and what little there was I just parsed the way I knew how--{% raw %}`JSONSerialization`{% endraw %}.
A few months later, I had to send some data back and I thought, "hey, maybe now is a good time to update my knowledge base here." My data wasn't particularly complex--a few values, nor did it have anything nested, but you know, start small.
So I watched a couple videos, read some blogs on how to do it, and away I went. For what I needed, it worked great, was easy to read by my fellow developers in our research lab and I felt quite proud of myself.
But had I really learned anything? No. I learned how to take some code someone else wrote and fit it into what I needed, which, don't get me wrong, is a good first step at learning something. I've never been good at taking something abstract and making sense of it. I need to see it in action, play with it and then, maybe after a few months of using, start to make sense of what I'm doing.
Last year I left the research lab I was working in and went to work in the private sector. It was here that I learned just how often big production apps use network calls to send and receive information. My old method of using NSObject and writing some parsing initializer got way too convoluted. I decided to make use of the benefits of Codable and haven't really looked back since. I went through some trial and error, but the place I started was with my original parsing project from my bootcamp. Taking weather data and using Codable to process it.
#####Step One: How do I use Codable again?!
By this point in my development career, [Apple Docs](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) didn't intimidate me as much as they used to. My thinking was that if I needed to really understand how to do this, I should get use to seeing Apple's intentions on how to use it. As it turns out, Apple had really good documentation and an example to follow and it managed to make the code above look incredibly simple.
What I needed to do, was create a struct with the values I wanted...{% raw %}
~~~swift
struct WeatherData {
var latitude: Double
var longitude: Double
var summaryDescription: String
var icon: String
var precipProbability
var temperature: Double
var humidity: Double
var windspeed: Double
}
~~~{% endraw %}
Now the problem is that while latitude and longitude are at what I call the "top-level" of the json coming in, the remainder of the items are in a nested dictionary called {% raw %}`currently`{% endraw %}. And while I could again go the way I did earlier with diving into a nested dictionary and pulling out relevant information, I do run into the issue where I may misspell something by making key stringly-typed. Apple helps us out with this by the use of CodingKeys.{% raw %}
~~~swift
// appending to my WeatherData model...
enum CodingKeys: String, CodingKey {
case latitude, longitude, currently // the currently case is necessary to go down one level to get into summary, temperature, etc...
}
~~~{% endraw %}
CodingKeys allow us to create an enum with the dictionary keys as the cases. Since the enum is a string, it allows us to edit the raw value of the case to match whatever the actual json dictionary key is. For instance, the word {% raw %}`windspeed`{% endraw %} is one word. I decide I want my coding key to be {% raw %}`windspeed`{% endraw %}. However when I look back into the json, the key they use is 'windSpeed{% raw %}`. I can edit the enum case to match the actual json key...
~~~swift
enum CodingKeys: String, CodingKey {
case latitude, longitude, currently
}
enum CurrentlyKeys: String, CodingKey {
case summary, icon, precipProbability, temperature, humidity, windspeed = "windSpeed"
}
You will notice that I have two sets of enums. CodingKeys
is my data at the top level of the json. CurrentlyKeys
is data within the currently
case.
At present, none of this will work if we try to decode. Simply because with haven't asked our model to conform to the Codable
protocol. Codable
is simply a type alias for both Encodable
and Decodable
protocols. If you're only receiving information, you can choose to implement on the Decodable
protocol. If you're sending, Encodable
, or if you think you might need both, conform to Codable
. Since I'm only receiving information, I'll stick with using Decodable
.
If you look at the bottom of Apple's documents, it will offer some insight into decoding manually, specifically if not all your information is on the same level (i.e. nested). Instead of making the struct conform to Decodable
, it uses an extension off of the struct to write the required decodable initializer. I've personally preferred this way because you never know when you might want to let your struct be able to use it's member-wise initializer somewhere down the line.
extension WeatherData: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)
let currently = try values.nestedContainer(keyedBy: CurrentlyKeys.self, forKey: .currently)
summaryDescription = try currently.decode(String.self, forKey: .summary)
icon = try currently.decode(String.self, forKey: .icon)
precipProbability = try currently.decode(Double.self, forKey: .precipProbability)
temperature = try currently.decode(Double.self, forKey: .temperature)
humidity = try currently.decode(Double.self, forKey: .humidity)
windspeed = try currently.decode(Double.self, forKey: .windspeed)
}
}
So what is happening here?
According to the Apple Docs regarding container
the data coming in is stored in a container, and can be referenced by the corresponding CodingKey
. So let values...
is asking the data to be sorted into the three CodingKeys categories we specified earlier. From there we can easily parse out latitude and longitude giving their type and the CodingKey assigned to it earlier.
However, all of the remaining values sit inside the currently dictionary, which is is also a container. So Decodable has another method called nestedContainer
which works much like container. It just needs a set of coding keys to use to sort out the information inside of it. So once I establish that currently
is a nested container using the CurrentlyKeys
, I can go in and parse the remaining values in my model. At the end, we have something along the lines of this:
struct WeatherData {
var latitude: Double
var longitude: Double
var summaryDescription: String
var icon: String
var precipProbability: Double
var temperature: Double
var humidity: Double
var windspeed: Double
enum CodingKeys: String, CodingKey {
case latitude, longitude, currently
}
enum CurrentlyKeys: String, CodingKey {
case summary, icon, precipProbability, temperature, humidity, windspeed = "windSpeed"
}
}
extension WeatherData: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)
let currently = try values.nestedContainer(keyedBy: CurrentlyKeys.self, forKey: .currently)
summaryDescription = try currently.decode(String.self, forKey: .summary)
icon = try currently.decode(String.self, forKey: .icon)
precipProbability = try currently.decode(Double.self, forKey: .precipProbability)
temperature = try currently.decode(Double.self, forKey: .temperature)
humidity = try currently.decode(Double.self, forKey: .humidity)
windspeed = try currently.decode(Double.self, forKey: .windspeed)
}
}
After that, it's a matter of taking the json data, converting it and using our Decoder
let weatherJson = """
{
"latitude": 37.8267,
"longitude": -122.4233,
"timezone": "America/Los_Angeles",
"currently": {
"time": 1557685055,
"summary": "Mostly Cloudy",
"icon": "partly-cloudy-day",
"nearestStormDistance": 0,
"precipIntensity": 0,
"precipProbability": 0,
"temperature": 53.72,
"apparentTemperature": 53.72,
"dewPoint": 50.31,
"humidity": 0.88,
"pressure": 1016.37,
"windSpeed": 6.11,
"windGust": 10.03,
"windBearing": 260,
"cloudCover": 0.9,
"uvIndex": 4,
"visibility": 10,
"ozone": 351.5
}
}
"""
let jsonData = weatherJson.data(using: .utf8)!
let decoder = JSONDecoder()
let currentWeather = try decoder.decode(WeatherData.self, from: jsonData)
print(currentWeather.latitude)
print(currentWeather.temperature)
I imagine there are other examples out there that follow this exact same model. I mean, it is how Apple's documents set it up. However, there are times at work where I have several nested layers of information, and using nestedContainer
over and over within the intializer makes an already involved extension more complex. So I prefer to attack it a different way. Each nested level gets it's own struct, it's own CodingKeys, and it's own extension. I prefer this because it keeps my structs small, and in my mind, easier to read. Allow me to refactor the above.
I'll start at the beginning with my WeatherData model
struct WeatherData {
var latitude: Double
var longitude: Double
var currentWeather: CurrentWeather
private enum CodingKeys: String, CodingKey {
case latitude
case longitude
case currentWeather = "currently"
}
}
extension WeatherData: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)
currentWeather = try values.decode(CurrentWeather.self, forKey: .currentWeather)
}
}
I prefer to have CurrentWeather as it's own struct. So here my top level has a reference to top level properties, my next level has a reference to only the properties within it, and so on an so forth. I have changed up what I'm sending back from the earlier model to keep things shorter. You'll also notice I included a way to handle optionals. In my line of work dealing with manufacturing data, we do have some models with more information than others and many of those fields are blank or nil.
struct CurrentWeather {
var summary: String
var temperature: Double
var windspeed: Double
var uvIndex: Int?
private enum CurrentWeatherCodingKeys: String, CodingKey {
case summary
case temperature
case windspeed = "windSpeed"
case uvIndex
}
}
extension CurrentWeather: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CurrentWeatherCodingKeys.self)
summary = try values.decode(String.self, forKey: .summary)
temperature = try values.decode(Double.self, forKey: .temperature)
windspeed = try values.decode(Double.self, forKey: .windspeed)
uvIndex = try values.decodeIfPresent(Int.self, forKey: .uvIndex)
}
}
For me, the breaking it down into separate models makes it easier for me to understand the layers. It also keeps it small. I generally keep all the models in the same file and label it something like WeatherDataResponse
.
I have noticed that my top level CodingKey has to be named CodingKeys
if you plan on not using an extension to write your own initializer and just have WeatherData
conform to Decodable. If I name it anything else, the playground gives me an error. Nested level names for the CodingKeys do not seem to matter. However, I generally like using extensions to keep my member wise initializer, so this isn't a problem for me.
All in all, this post was just as much about me explaining Codable (well, mostly Decodable...) to myself as it was about explaining it to others. I hope, perhaps, that some find some use out of it, or at the very least, encourages you to learn something new. Any constructive feedback is welcome.
Top comments (0)