Migrating to Codable from a third party parsing library

Eleni Papanikolopoul
May 29 · 6 min read

Nowadays, JSON is one of the most widely used format in order to exchange data between a browser and a server. JSON parsing has always been one of the most challenging endeavours in iOS development since JSONSerialization, an option offered by the Foundation framework, is pretty verbose and cumbersome to both write and read. NSCoding is another option that is used essentially for archiving and distribution purposes. Both solutions include writing redundant and boilerplate code, introducing a significant degree of cognitive overhead.

Therefore, there have been many third party libraries that facilitate and simplify the JSON serialization and de-serialization. One of those libraries is Argo which allows to parse JSON in a functional and type-safe way, and this is the framework we had been using so far at Workable.

However, at WWDC 2017 and the arrival of Swift 4, Apple introduced the new Codable protocol as the suggested solution for encoding and decoding. Codable is basically the combination of Encodable and Decodable protocols, it is easy to adopt and provides support for class, struct and enum as well.

In this article we will explore the process of migrating from Argo to Codable, the challenges we went through and how we managed to overcome them by providing some helper protocols in order to achieve a gradual and smoother migration. Note that we will focus primarily in the decoding part of models.

The challenge 🤔

When we first started thinking of the migration, we came to the horrifying 😱 realisation that we had so many models in our project that was impossible to simply remove Argo and write the decoding code with Codable from scratch. Additionally, the dependency graph between models was so complicated, that would require to implement the decoding of many models at once. This means it would take an extensive amount of work hours, or even better days, during which the project would not even compile and although we did have unit tests, that was simply not an option.

The solution 🙌🏻

Therefore, we came up with the idea to implement an intermediate step: Our models would be Argo Decodable but simultaneously would be able to be decoded with Codable as well. This means that an Argo decodable model would also be able to have nested models that would be decoded with Codable and the other way round. A decoded with Codable model would be able to have nested models that are Argo decodable. Hence, we would be able to gradually retire Argo and the project would compile every step of the way. Now, that was great in theory, but as Jan L.A. van de Snepscheut said: “In theory there is no difference between theory and practice. In practice there is”. So let’s see how we managed to implement this in practice.

Argo Decoding

Let’s see first how we would decode a simple User type using Argo.

We want to parse the following JSON file:

So the User model looks like this and is decoded with Argo as follows:

SocialProfile and EmployeeLevel are also decoded with Argo for the above code to work.

Use Swift.Decodable models inside Argo.Decodable models

Now what we want to try is to make Social Profile decoded with Codable and use it inside User. So Social Profile is decoded with Codable as follows:

struct SocialProfile: Swift.Decodable {   let type: String
let url: String
let username: String?
let name: String?
}

and EmployeeLevel which is an enumeration as follows:

extension EmployeeLevel: Swift.Decodable {}

If the keys inside the JSON are the same as the properties of the struct or enum there is no need to implement the init(from decoder: Decoder) method and this is exactly the magic ✨ of the Codable protocol. Now let’s see how we can use the SocialProfile type inside the User type. For this purpose, we implemented a protocol to be conformed by models that are already Swift.Decodable, in order to be used inside Argo decodable types, which we named SwiftToArgoDecodable and is the following:

So what we basically do inside the protocol extension, is providing our own custom implementation of the decode(_ json: JSON) function of Argo.Decodable protocol by using the JSONDecoder of Codable and thus conforming to the Argo.Decodable protocol. In detail, what we actually do is using Argo’s JSON object, we convert it to jsonData and use it inside the JSONDecoder(). And voila! Now we can erase the decoding code of SocialProfile with Argo, and simply make it conform to SwiftToArgoDecodable protocol as follows:

struct SocialProfile: Swift.Decodable, SwiftToArgoDecodable {  let type: String
let url: String
let username: String?
let name: String?
}

Although SocialProfile is a Swift decodable model, it can now be used inside other Argo decodable models, such as User in our case. Quite simple right? Now let’s see how we can do the opposite.

Use Argo.Decodable models inside Swift.Decodable models

Now we want to use the User type that is Argo decodable inside another UserList type which is Swift decodable:

struct UserList: Swift.Decodable {  let id: Int
let users: [User]
}

For this purpose we implemented a new protocol to be conformed by models that are already Argo decodable (User), in order to be used inside Swift decodable types (UserList), which we named ArgoToSwiftDecodable and is the following:

Now the protocol declaration is exactly the same with the SwiftToArgoDecodable, which makes sense considering that in both cases the models should be both Argo and Swift decodable. However, what we do differently here, is that we implement the init(from decoder: Decoder) function of Codable with our own custom implementation.

Normally, inside this function we would use a KeyedDecodingContainer where we would specify the CodingKeys. However, since here we don’t know the coding keys we want a more generic implementation. Therefore we use a SingleValueEncodingContainer which according to Apple’s specification is:

A container that can support the storage and direct encoding of a single non-keyed value.

And in this line of code:

let payload = try container.decode(JSON.self)

we decode the payload as a JSON which is an enumeration provided by Argo:

// A type safe representation of JSON.
public enum JSON {
case object([String: JSON])
case array([JSON])
case string(String)
case number(NSNumber)
case bool(Bool)
case null
}

However if we run the aforementioned code we will get the following error:

This happens because the compiler cannot understand which decode function to use, since Argo.JSON is not Swift.Decodable. Therefore, we have to “help” the compiler a tiny bit by implementing the init(from decoder: Decoder) inside an Argo.JSON extension so as to make it conform to Swift.Decodable as follows:

As you can notice, we specify for each possible type (boolean, NSNumber, array, dictionary etc.) how the decoding should be and we throw a decoding error in case a type that is not included in any of the types specified comes up.

For other third party libraries, we would probably have to create a custom type like Argo.JSON and make it conform to Swift.Decodable in a similar way.

One last step is to make the User type conform to the ArgoToSwiftDecodable protocol instead of Argo.Decodable that was before as follows and we are ready.

Now UserList type can be decoded with Codable even though it contains a model of type User which is Argo decodable (which in extension contains a model of SocialProfile type which is decoded with Codable).

Conclusion

By implementing those two protocols and making our models conform to them we have managed to interchangeably use models that are decoded with both Codable and Argo. Therefore, during our migration, we can start gradually decode our models with Codable, and the project will compile at all times. What is more, the above methodology can be used for any third party libraries that are used for JSON parsing 🎉. Just keep in mind that is doesn’t all have to happen at once and you can achieve a smoother migration by making your models able to be decoded with both ways.

Hope it proves helpful!🙏🏽

You can find the example project here.

Special thank to George Tsifrikas and Kostas Kremizas for their contribution!

Thanks to George Tsifrikas and Kostas Kremizas

Eleni Papanikolopoul

Written by

Senior iOS Developer @workable

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade