Swift Codable Part II

Insight

Swift Codable Protocol Part II

Working with Codable

Office

  • United States

Services

Author

  • Justin Powell

Codable is a powerful tool that was introduced into Swift about a year ago. While there are many articles describing how to implement Codable in your projects, I will dive a little deeper and share my experience using Codable while working on a large-scale application at Wunderman Thompson Apps. I will first go over the initial implementation process, then share some of the bottlenecks we found and solved.

Getting Started

When converting objects to adhere to the Codable protocols, some obstacles will definitely pop up. Consider this example JSON response for a Photo object from a backend service. There are some basic values like an id, an image URL, an optional caption string, some metadata about the photo, and an id and name for the photographer who captured the image.

{
   "id": 1234,
   "src": "//api.someHost.com/version/someImageName.jpg",
   "caption": null,
   "meta": {
       "width": 1980,
       "height": 1080,
       "color": "#34ab34"
   },
   "photographer": {
       "id": 456,
       "name": "Photo McPhotoface"
   }
}
 

*Notice how the image URL doesn’t have a scheme like “https” attached to it, that will be important later.

Now consider the following Photo and Photographer objects, that do not conform to Codable:

struct Photo {
 
   let id: Int
   let sourceURL: URL
   let caption: String?
   let width: CGFloat
   let height: CGFloat
   let color: UIColor
   let photographer: Photographer
 
   init?(json: [String: Any]) {
       guard let id = json["id"] as? Int,
           let sourceString = json["src"] as? String,
           var sourceComponenets = URLComponents(string: sourceString),
           let metaJSON = json["meta"] as? [String: Any],
           let photographerJSON = json["photographer"] as? [String: Any] else { return nil }
 
       if sourceComponenets.scheme == nil {
           sourceComponenets.scheme = "https"
       }
 
       guard let sourceURL = sourceComponenets.url else { return nil }
 
       let caption = json["caption"] as? String
 
       let width = metaJSON["width"] as? CGFloat ?? 0
       let height = metaJSON["height"] as? CGFloat ?? 0
 
       guard let colorString = metaJSON["color"] as? String else { return nil }
 
       let color = UIColor(hex: colorString)
 
       guard let photographer = Photographer(json: photographerJSON) else {return nil}
 
       self.id = id
       self.sourceURL = sourceURL
       self.caption = caption
       self.width = width
       self.height = height
       self.color = color
       self.photographer = photographer
   }
}
 
struct Photographer {
   let id: Int
   let name: String
 
   init?(json: [String: Any]) {
       guard let id = json["id"] as? Int,
           let name = json["name"] as? String else {return nil}
 
       self.id = id
       self.name = name
   }
}

Depending on the size of our objects, the traditional key-value parsing can get messy and lead to a lot of code and mistakes since there are many string literals that must be checked. For instance, a typo could return a nil object even if all the data is in fact returned from the API.

Setting up Codable

Many applications only consume (and do not generate) data, and thus will only need to adhere to the Decodable protocol within Codable. Let’s create the Decodable Photo object, based on the same data being parsed into the OldPhoto object. First, add the Decodable adherence to the struct declaration as I’ve shown below.

struct Photo: Decodable {
   // ...
}

Now that we’ve done that, the Photo object should conform to Decodable, right? Wrong. With the addition of the Codable protocol, basic types like Int, String, and URL are already handled (see Encoding and Decoding Automatically).

Objects like UIColor or any custom objects will need some extra work to make the new encompassing object fully Decodable. Since the Photo object contains a Photographer object, the Photographer object must conform to Decodable.

struct Photographer: Decodable {
   // ...
}

Next, we must change the color property on Photo to a String since the API returns a hex string. Renaming the property to colorString and making it private will provide clarity and make it easier to fix build errors in the future.

// Old color property
let color: UIColor
 
// New color property
private let colorString: String

In order to avoid errors from changing property names, we will add an extension on Photo that converts the hex string to a UIColor.

extension Photo {
   var color: UIColor {
       return UIColor(hex: colorString)
   }
}
 

Now that we’ve added the extension, the project should build without errors and the Photo object should return objects how it did before.

Decoding a Photo Object

Next, decode a Photo object from the JSON data we receive from the backend.

    1. In the init method for Photo, take out all the key-value code.
    2. Create a do-catch block and set self (since this is a struct) to the output of JSONDecoder.decode.

a. The catch block gives us the opportunity to print out errors and/or use assertionFailures to catch decoding errors that can help us fix our objects.

  1. Set up the decode method with the Photo type and the serialized JSON;.data is a helper method that serializes JSON into Data.
init?(json: [String: Any]) {
   // 2.
   do {
       // 3.
       self = try JSONDecoder().decode(Photo.self, from: json.data)
   } catch {
       assertionFailure("Error decoding Photo: \(error)")
       return nil
   }
}

Now we have a Photo object parsed using the decoder (which will currently fail when the init method returns nil). The error in the catch statement will give us details, in this case, because it can’t find a key for sourceURL. To fix this, add an enum called “CodingKeys” to the Photo struct (of type String and conforming to the protocol CodingKey). The case names we add need to match the property names and the rawValue needs to match the key we get in the JSON.

Since the meta properties are in a nested JSON object, we can handle those two different ways, depending on how we want the Photo object to look. We could implement them either with all the meta properties in a separate “Meta” struct or keep the meta properties on our Photo object. There are pros and cons to both, so let’s dive into them.

Nesting Strategies

Nested Structs

One way to handle nested data is to create nested structs in objects. This strategy can make our objects more structured but will also lead to more small changes throughout the project. Let’s make nested structs inside of Photo to handle the metadata nested in the JSON response.

  1. Create a new struct inside of the Photo struct and name it “Meta,” it will need to conform to Decodable for the Photo struct to remain Decodable.
  2. Move the width, height, and colorString properties into the Meta struct.
  3. Make a new enum in Photo.Meta called “CodingKeys.”
  4. Move the relevant coding key cases and values from the Photo.CodingKeys into another “CodingKeys” enum in Meta.
  5. Create a new Meta property on the Photo struct.
  6. Add a case for meta in Photo.CodingKeys.
struct Photo: Decodable {
           // 1.
   struct Meta: Decodable {
       // 2.
       let width: CGFloat
       let height: CGFloat
       private let colorString: String
 
      // 3.
       enum CodingKeys: String, CodingKey {
           // 4.
           case width
           case height
           case colorString = "color"
       }
   }
 
   let id: Int
   let sourceURL: URL
   let caption: String?
   // 5.
   let meta: Meta
   let photographer: Photographer
 
   // Need the CodingKeys enum because the API keys do not match our property names
   enum CodingKeys: String, CodingKey {
       case id
       case sourceURL = "src"
       case caption
       // 6.
       case meta
       case photographer
   }
}

After doing this we either need to build more properties in the extension to get the meta properties, or go through the app and replace the old references to properties. Since Xcode will not build the project and display errors for the missing properties, they should be easy to identify. Instances like photo.width will become photo.meta.width.

Notice the CodingKeys enum case names match the property names, and if there is a key that doesn’t match, we set the raw value to the JSON key. In the examples above we set the sourceURL case to "src" and in the Meta struct for CodingKeys we set the colorString case to “color.” This is helpful when dealing with an API whose keys change with version changes.

We will also need to update the extension to Photo.Meta since the colorString property is now inside of the Meta struct where the Photo object cannot access it directly.

extension Photo.Meta {
   var color: UIColor {
       return UIColor(hex: colorString)
   }
}

Since the JSON keys for the Photographer object match the property names in the object, we don’t need to add a CodingKeys enum, which means the Photographer object is good to go.

Flat Objects with Multiple CodingKeys

The other strategy is keeping the object flat without nested structs. This strategy allows us to keep references to the properties throughout the project, which can save a lot of time if they are numerous.

In this strategy, we first keep the CodingKeys enum we made in the previous example. Then we make a second enum that is of type String and conforms to CodingKey, calling it “MetaCodingKeys.” Add the meta properties as cases and set their rawValues accordingly.

enum MetaCodingKeys: String, CodingKey {
     case width
     case height
     case colorString = "color"
}

Once we’ve completed the CodingKeys enum, Xcode will throw an error saying that Photo does not conform to Decodable. To get rid of this error, we must implement init(from decoder: Decoder).

    1. Write out the method declaration (which should autocomplete).
    2. Make a container from the decoder that uses the CodingKeys enum.
    3. Once the container is set up start initializing values.
    4. For optional properties we should use the built-in decodeIfPresent method, otherwise the app might crash.

a. Another approach would be to use decode(String?.self, forKey: .someKey).
b. If we are not guaranteed to get the key in the response we should use
decodeIfPresent. This is a failsafe way of decoding optional properties unless the data is corrupted.

  1. Create a new container to parse the metadata in the JSON using the method called nestedContainer, from the current container.
  2. Pass in the MetaCodingKeys type for the .meta key from the CodingKeys enum.
  3. With this nested container we can go through and initialize the rest of our properties
// 1.
public init(from decoder: Decoder) throws {
     // 2.
     let container = try decoder.container(keyedBy: CodingKeys.self)
 
     // 3.
     id = try container.decode(Int.self, forKey: .id)
     sourceURL = try container.decode(URL.self, forKey: .sourceURL)
 
     // 4.
     caption = try container.decodeIfPresent(String.self, forKey: .caption)
     photographer = try container.decode(Photographer.self, forKey: .photographer)
 
     // 5 and 6.
     let metaContainer = try container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
     // 7.
     width = try metaContainer.decode(CGFloat.self, forKey: .width)
     height = try metaContainer.decode(CGFloat.self, forKey: .height)
     colorString = try metaContainer.decode(String.self, forKey: .width)
}

The upside of this approach is creating flatter objects that can automatically decode nested data. However, the downside is having to write the custom decoder and ending up with more code.

In projects at Wunderman Thompson Apps, we make objects conform to Decodable with either of these strategies on a case by case basis. Personally, I like the nested structs strategy because it makes objects cleaner, and (in most cases) there is no need to implement the custom decoder method. Here is a final comparison of what the updated Photo object looks like using Decodable in both the nested and flat configurations.

Old Photo with Old Parsing

struct Photo {
 
   let id: Int
   let sourceURL: URL
   let caption: String?
   let width: CGFloat
   let height: CGFloat
   let color: UIColor
   let photographer: Photographer
 
   init?(json: [String: Any]) {
       guard let id = json["id"] as? Int,
           let sourceString = json["src"] as? String,
           var sourceComponenets = URLComponents(string: sourceString),
           let metaJSON = json["meta"] as? [String: Any],
           let photographerJSON = json["photographer"] as? [String: Any] else { return nil }
 
       if sourceComponenets.scheme == nil {
           sourceComponenets.scheme = "https"
       }
 
       guard let sourceURL = sourceComponenets.url else { return nil }
 
       let caption = json["caption"] as? String
 
       let width = metaJSON["width"] as? CGFloat ?? 0
       let height = metaJSON["height"] as? CGFloat ?? 0
 
       guard let colorString = metaJSON["color"] as? String else { return nil }
 
       let color = UIColor(hex: colorString)
 
       guard let photographer = Photographer(json: photographerJSON) else {return nil}
 
       self.id = id
       self.sourceURL = sourceURL
       self.caption = caption
       self.width = width
       self.height = height
       self.color = color
       self.photographer = photographer
   }
}

Nested Decodable Photo

struct Photo: Decodable {
 
   struct Meta: Decodable {
       let width: CGFloat
       let height: CGFloat
       private let colorString: String
 
       enum CodingKeys: String, CodingKey {
           case width
           case height
           case colorString = "color"
       }
   }
 
   let id: Int
   let sourceURL: URL
   let caption: String?
   let meta: Meta
   let photographer: Photographer
 
   enum CodingKeys: String, CodingKey {
       case id
       case sourceURL = "src"
       case caption
       case meta
       case photographer
   }
 
   init(from decoder: Decoder) throws {
       let container = try decoder.container(keyedBy: CodingKeys.self)
       id = try container.decode(Int.self, forKey: .id)
       let urlString = try container.decode(String.self, forKey: .sourceURL)
       sourceURL = URL(from: urlString)
       caption = try container.decodeIfPresent(String.self, forKey: .caption)
 
       meta = try container.decode(Meta.self, forKey: .meta)
       photographer = try container.decode(Photographer.self, forKey: .photographer)
   }
 
   init?(json: [String: Any]) {
       do {
           self = try JSONDecoder().decode(Photo.self, from: json.data)
       } catch {
           print("Error decoding Photo: \(error)")
           return nil
       }
   }
}
 
extension Photo.Meta {
   var color: UIColor {
       return UIColor(hex: colorString)
   }
}

Flat Decodable Photo

struct Photo: Decodable {
 
   let id: Int
   let sourceURL: URL
   let caption: String?
   let width: CGFloat
   let height: CGFloat
   let colorString: String
   let photographer: Photographer
 
   enum CodingKeys: String, CodingKey {
       case id
       case sourceURL = "src"
       case caption
       case meta
       case photographer
   }
 
   enum MetaCodingKeys: String, CodingKey {
       case width
       case height
       case color
   }
 
   init(from decoder: Decoder) throws {
       let container = try decoder.container(keyedBy: CodingKeys.self)
       id = try container.decode(Int.self, forKey: .id)
       caption = try container.decodeIfPresent(String.self, forKey: .caption)
 
       let urlString = try container.decode(String.self, forKey: .sourceURL)
       sourceURL = URL(from: urlString)
 
       photographer = try container.decode(Photographer.self, forKey: .photographer)
 
       let metaContainer = try container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
       width = try metaContainer.decode(CGFloat.self, forKey: .width)
       height = try metaContainer.decode(CGFloat.self, forKey: .height)
       colorString = try metaContainer.decode(String.self, forKey: .color)
   }
   
   init?(json: [String: Any]) {
       do {
           self = try JSONDecoder().decode(Photo.self, from: json.data)
       } catch {
           print("Error decoding Photo: \(error)")
           return nil
       }
   }
}
 
extension Photo {
   var color: UIColor {
       return UIColor(hex: colorString)
   }
}

Getting objects to conform to the new Codable protocol is not super challenging and at Wunderman Thompson Apps, we prefer it over the dictionary parsing methods of the past. Even though using Codable is very powerful, we ran into some caveats I would like to share to help others who run into similar problems.

Common Codable Caveats

Most of the caveats we ran into came from data issues and dealing with different versions of an API. First, some JSON keys were returned differently from different API’s that were intended to be parsed into the same object. Then we discovered that most of the media URLs we parsed were missing schemes. Additionally, some of the data we needed was returned in an unexpected format. Finally, some data was wrapped in a parent dictionary causing our Decodable objects to fail during the decoding process.

Different Keys for the Same Object

The different versions of the API would spit out different keys for the same values we needed, or a different parent key for an object we would be trying to parse. Let’s consider a simpler Photo object with just the sourceURL.

struct Photo: Decodable {
    let sourceURL: URL
}

Now how would we make this decodable if we are trying to support two versions of the API at one time? Our solution was a flyweight pattern, which we called “skeletons.” Skeletons let us model the specific API version objects and convert them to the object we use throughout the app. Here is an example of what the skeletons would look like:

struct PhotoV1Skeleton: Decodable {
   let source_url: URL
}
 
struct PhotoV2Skeleton: Decodable {
   let src: URL
}

Once we have the skeletons in place we need to build out separate initializers on the Photo object for them. We created an extension on Photo that had different init methods for each skeleton, where we then map corresponding properties.

extension Photo {
   init(from skeleton: PhotoV1Skeleton) {
       self.sourceURL = skeleton.source_url
   }
 
   init(from skeleton: PhotoV2Skeleton) {
       self.sourceURL = skeleton.src
   }
}

This strategy provides a really clean code and eliminates the need for CodingKeys or custom decoder methods. We model our skeletons after the API, then map all of the values back to our own objects. Alternatively, we can choose keys that match different versions of the API for our CodingKeys enum and have fewer skeletons.

Missing Schemes

The next issue we ran into was that URLs for our media objects didn’t have a scheme attached, so we needed to build out our own custom URL initializer that added the “https” scheme. This particular issue forced us to write custom decoder methods for our objects, as seen below:

public init(from decoder: Decoder) throws {
   let container = try decoder.container(keyedBy: CodingKeys.self)
 
   let sourceURLString = try container.decode(String.self, forKey: .sourceURL)
   sourceURL = URL(attachingScheme: sourceURLString)
}

Unexpected Data Format

Codable can help you manage situations where your API returns completely unexpected data types. In our case, we were getting dictionaries where we expected to get arrays. Imagine that there is an Album object that holds a bunch of photos, and the API returns a dictionary of photos (which is not able to be changed to an array). We created a Decodable object that could parse a dictionary and return an array of any Decodable type.

public struct HashObject<T: Decodable>: Decodable {
 
   private struct HashCodingKeys: CodingKey {
       var stringValue: String
 
       init?(stringValue: String) {
           self.stringValue = stringValue
       }
 
       var intValue: Int?
 
       init?(intValue: Int) {
           self.init(stringValue: "")
           self.intValue = intValue
       }
   }
 
   static func decodeHash(from decoder: Decoder) throws -> [T] {
       let container = try decoder.container(keyedBy: HashCodingKeys.self)
       var objects = [T]()
       for key in container.allKeys {
           do {
               let object = try container.decode(T.self, forKey: key)
               objects.append(object)
           } catch let error {
               print(error)
           }
       }
 
       return objects
   }
}

Now, when we decode an Album, we use this to generate an array of photos. The downside to this solution is that we need to sort the array after you create it; however, since it does allow us to use the photos in an array, that compromise seems reasonable. Below, we see how our HashObject returns an array of photos.

public struct Album: Decodable {
   public let photos: [Photo]
 
   private enum CodingKeys: String, CodingKey {
       case photos
   }
 
   public init(from decoder: Decoder) throws {
       let container = try decoder.container(keyedBy: CodingKeys.self)
 
       photos = try HashObject<Photo>.decodeHash(from: container.superDecoder(forKey: .photos)).sorted { $0.id < $1.id }
   }
}

We pass in a type that conforms to Decodable and calls the static method decodeHash. Then we use the container’s super decoder to get the entire hash back. Then we can sort the array if necessary.

Wrapped Objects

Another issue we ran into was wrapped objects in the response. For example, a photo object whose key was “content.” While this is annoying for fetching one object, we needed to deal with it. Using generics, we can create a container struct that will return the object that we want:

struct ContentContainer<T: Decodable> {
   let content: T
}

When decoding something like a Photo we use the type ContentContainer so the decoding will work automatically. Once our container object is decoded we can access the generic object we passed in.

All in all, our team was very happy to clean up our objects and remove a ton of code using Codable. It saves us time and headaches developing and debugging and allows us to see if we are missing data in a request easily (since it will throw exceptions when decoding). I would highly encourage everyone to use Codable in their projects. It’s a powerful tool that we can use to help make our apps better for users and developers.

Please provide your contact information to continue. Detailed information on the processing of your personal data can be found in our Privacy Policy.

Related Content

In The Press

Women in Innovation

Sherine Kazim, NA Chief Experience Officer shares career lessons learned including the importance of being a mentor and insights on ethical design.
Read More
profile picture of Sherine Kazim