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.
Swift Codable Protocol Part II
Working with Codable
Office
- United States
Services
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.
- In the init method for Photo, take out all the key-value code.
- 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.
- 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.
- 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.
- Move the width, height, and colorString properties into the Meta struct.
- Make a new enum in Photo.Meta called “CodingKeys.”
- Move the relevant coding key cases and values from the Photo.CodingKeys into another “CodingKeys” enum in Meta.
- Create a new Meta property on the Photo struct.
- 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).
- Write out the method declaration (which should autocomplete).
- Make a container from the decoder that uses the CodingKeys enum.
- Once the container is set up start initializing values.
- 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.
- Create a new container to parse the metadata in the JSON using the method called nestedContainer, from the current container.
- Pass in the MetaCodingKeys type for the .meta key from the CodingKeys enum.
- 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
Business not as usual

Icons, RockStars, & Innovators: Ezinne Okoro
