Swift 4 decoding JSON using Codable

swift-4-decoding-json-codable-header.png

Learn to use Codable to handle JSON decoding in Swift 4\. This tutorial uses a sample app (provided) to highlight key code features.

Introduction

Introduction

Swift 4 was released along with iOS11 last September. It focuses on improving the stability of the ABI (application binary interface). This meant less drastic changes as compared to when we made the move from Swift 2 to Swift 3. My favourite new feature in Swift 4 is the addition of the Codable extension. Codable makes it easy to parse JSON into structs and use throughout your code, no more messing around with dictionaries. Swift 4.1 makes some changes to the Codable extension that make it even more powerful, but more on that later.

Prerequisites

It’s not a requirement but cloning the completed example project may make the tutorial easier to follow. Example Project

Main content

Understanding your JSON

For this tutorial I will be reading in JSON from a local file. The principles of using the Codable extension do not change whether you are reading in data from a local file or one on a server.
The stub data within the project contains a file londonWeather.json This is a real response from the open weather map API.

There are two key concepts in understanding your JSON file: you need to understand what is a key/value pair and what is a key/object pair. The stub data contains plenty of examples of either of these. If we look at the highest level of the object the first two keys are examples of key/value.

1"id":2643743,
2    "name":"London"

The key maps directly to the value, key being the string on the left of the colon and the value being the type (such as string integer) on the right.
Key/object pairs are those that don’t map directly to types in JSON. The object itself usually contains a set of key/values.

1"coord": {
2      "lon":-0.13,
3      "lat":51.51
4    },

The key in the above example is coord but within the object there are two key/value pairs. Another example of a key/object pair is one that maps to an array. The principle is the same here but it can map to more than one object.

1"weather": [{
2       "id":300,
3      "main": "Drizzle",
4      "description": "light intensity drizzle",
5      "icon":"09d"
6    }]

Converting your JSON to structs

The next task is to create your model. Start by taking your high level object. In the example project this is within the CurrentWeather.swift file.

1struct CurrentWeather: Codable { //1
2        // 2
3        let coord: Coord
4        // 3
5        let weather: [WeatherDetails]
6        // 4
7        let base: String
8        let main: Main
9        let visibility: Int
10        let wind: Wind
11        let clouds: Clouds
12        let dt: Int
13        let sys: Sys
14        let id: Int
15        let name: String
16    }
  • 1 – You must make sure that the struct conforms to the Codable protocol.
  • 2 – The variable coord maps to a key/object pair in the JSON. You will need to create a new structure that also conforms to Codable called Coord. More on this below.
  • 3 – The variable weather maps to an array of objects in the JSON. WeatherDetails is a struct that also conforms to Codable.
  • 4 – The variable base maps to a key/value pair.

When we decode the JSON, Codable will map each of the variable names to the keys in the JSON and assign the value to it. If the key contains an object, it will dive down into the struct and decode that as well. For example Coord is another struct that conforms to Codable:

1struct Coord: Codable {
2        let lon: Double
3        let lat: Double
4    }

It’s that simple to set up your basic structs to conform to Codable. You need to make sure that your type matches the one the JSON will supply, such as String, Double (for floating points), Int and so on.

Variable names using coding keys

It’s important to make sure that your variable names match to that of the keys within the JSON. This makes Codable simple to use in cases where we don’t care how the Swift variables are named, but that’s a rarity. JSON styling tends to prefer snake case whereas Swift tends to be camel case. We can extend Codable to use Coding Keys. We can use these to map the key within the JSON to our own variable name.

1enum CodingKeys: String, CodingKey {
2      // 1
3      case temp
4      case pressure
5      case humidity
6      // 2
7      case tempMin = "temp_min"
8      case tempMax = "temp_max"
9    }
  • 1 – We don’t want to map these to anything in the JSON as they are already equivalent.
  • 2 – We want to map these from temp_min to our camel case styling tempMin

When we do this we have to include all the keys whether we are mapping them or not. We also have to include the init and encode functions that can be found within the Main.swift file in the example project.

Variables, constants and optionals

The example project has set everything to be a constant (let). It’s important to note that we can also set them to be variables if we are going to be manipulating the variables at a later date.
Understanding your API end point is also important, understanding whether keys will always be present or only in certain circumstances. Codable will fail to parse the JSON if it searches for a key that it’s expecting but does not find it. This will result in none of the data being available so we want to avoid this wherever possible. We can use optionals (more detail on optionals can be found here) to tell Codable that something may or may not exist when we decode the JSON.

1let tempMin: Double?

If you’re using coding keys remember to replace:

1values.decode(Double.self, forKey: .humidity)

With:

1values.decodeIfPresent(Double.self, forKey: .humidity)

If you are using optionals.

Decoding the JSON

Now we need to retrieve the JSON from the remote server and convert the data into our structures. To do this create an APIManager that will handle requests to end points and return objects.

1func getWeather(completion: @escaping (_ weather: CurrentWeather?, _ error: Error?) -> Void) {
2            // 1
3            getJSONFromURL(urlString: stubDataURL) { (data, error) in
4                guard let data = data, error == nil else {
5                    return completion(nil, error)
6                }
7                // 2
8                self.createWeatherObjectWith(json: data, completion: { (weather, error) in
9                    guard let error == nil else {
10                        print("Failed to convert data")
11                        return completion(nil, error)
12                    }
13                    return completion(weather, nil)
14                })
15            }
16        }
  • 1 – Calls a function passing a URL that will return a Data object. The logic in this function is a standard URLSession request.
  • 2 – A call to a function that will convert the data to our CurrentWeather object. Shown below:
1private func createWeatherObjectWith(json: Data, completion: @escaping (_ data: CurrentWeather?, _ error: Error?) -> Void) {
2      do {
3          // 1
4          let decoder = JSONDecoder()
5          decoder.keyDecodingStrategy = .convertFromSnakeCase
6          // 2
7          let weather = try decoder.decode(CurrentWeather.self, from: json)
8          return completion(weather, nil)
9        } catch let error { // 3
10          print("Error creating current weather from JSON because: \(error.localizedDescription)")
11          return completion(nil, error)
12        }
13    }
  • 1 – Setting the decoder to use a coding strategy convertFromSnakeCase will mean that Codable will automatically map our camel case variables to the JSON snake case variables.
  • 2 – This is where we try to convert our data using the JSONDecoder. We tell the decoder what we are trying to convert the JSON to. In our case this is CurrentWeather.self. This is the high level struct that we created earlier. We also need to provide the data object (json).
  • 3 – If we haven’t set up our structs correctly, for example including a variable that isn’t in the data and isn’t considered optional or providing an incorrect type the decoder will fail. This will throw an error that we can catch here for debugging purposes.

We should be aware that because the JSON decoder can throw an error our object will be an optional. We can assign it to a non optional within the controllers that we wish to use it.
Inside our view controllers we need to call our apiManager and handle the response.

1apiManager.getWeather() { (weather, error) in
2      if let error = error {
3        print("Get weather error: \(error.localizedDescription)")
4        return
5      }
6      guard let weather = weather  else { return }
7      print("Current Weather Object:")
8      print(weather)
9    }

We return the error from the API manager to here as well so that we can tell the user something has gone wrong. Note that error is optional so it won’t necessarily exist here. Otherwise we have our weather object that we can unwrap using a guard and if we wish assign it to a variable within the class.

Conclusion

Codable is powerful in helping us decode (and also encode) JSON, it makes it quick and simple to get data from an API. The thing you have to do is understand your JSON data. Knowing what is optional/non-optional and whether something is a String, Double or Int before you get started can save you lots of time in the long run and reduce your number of bugs.