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 POSSIBLE Mobile. I will first go over the initial implementation process, then share some of the bottlenecks we found and solved.
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.
*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:
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 like I’ve shown below.
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.
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.
In order to avoid errors from changing property names, we will add an extension on Photo that converts the hex string to a UIColor.
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.
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 that 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.
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.
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.
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.
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.
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 POSSIBLE Mobile, 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 comparisons of what the updated Photo object looks like using Decodable in both the nested and flat configurations.
Old Photo with Old Parsing
Nested Decodable Photo
Flat Decodable Photo
Getting objects to conform to the new Codable protocol is not super challenging and at POSSIBLE Mobile, 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.
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:
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.
This strategy provides 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 less skeletons.
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:
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.
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.
We pass in a type that conforms to Decodable and call 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.
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:
When decoding something like a Photo we use the type ContentContainer<Photo> 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.