Codables: The Basics

It has been a long time we used stuff like NSJSONSerialization or NSPropertyListSerialization and dropped the NS prefix in favor of Swift. It helped converting data from or to JSON / Property List. Creating an instance of your model classes or serialize them to one of the formats would require writing a lot of additional code to convert the values to the correct data types.

Well, the time of these has come to an end and since Swift 4 we got a hand full of new protocols that allow the compiler to generate all the necessary code in the background. But we will still be able to customize the behaviours. More on that in a later chapter. Let us begin with the basics.

The Protocols

There are only two (or three) protocols that you really have to care about: Decodable and Encodable. And Codable, but that is only a type alias for Decodable & Encodable so it is just a simpler annotation when you want a model do be de- and encodable.

Decodable

This is the protocol when you want to decode data to your model. When you work with external Web-APIs which are JSON, you will most likely use Decodable to work with you model within your app.

Encodable

For the serialization part, meaning you want to store your model data in a file or send it out to an API, Encodable is you protocol of choice.

Codable conformant types

Most of the data types you get from the Swift Standard Library are conformant to Decodable and Encodable. You can find a complete list in the documentation provided by Apple. I linked the conformant types for Decodable, but all of these types also work for Encodable.

As you can see, event objects like CGPoint and CGSize are listed and completly conform to the Codable protocol. But they are represented as an array of float values. CGRect is represented as an array of arrays with float values. This may not be the ideal way of  representing the data, but it could be enough if you just want to store data in a local file instead of accessing a self-explaining API.

Date and Data are special snowflakes in the Codable space. At least if you want to use a JSONDe-/Encoder. These Coders special settings to modify the strategy on how a Date will be (de-)serialized. ISO-8601 formatted? UNIX timstamp? No problem at all. You can even add your own strategy. More on this in a later chapter.

Example

For convenience I will mostly use JSON in my examples because it is more readable than a full fledged property list.

Lets take this product list as a short example to introduce you to Codables:

let jsonString = """
[{
  "title": "Awesome new Book",
  "price": 12.99,
  "currency": "EUR"
},{
  "title": "Fantastic Movie",
  "price": 9,
  "currency": "USD"
}]
"""

Our goal is to parse this data that can be extended in the future and work with a Product model within my app.

Model

struct Product: Codable {
    let title: String
    let price: Double
}

Let us leave out the currency property at this step. We get back to it in a minute.

Decode JSON

To decode our jsonString, we have to convert it to a Data object

let jsonData: Data = Data(jsonString.utf8)

I'm doing it this way, because jsonString.data(using: .utf8) would result in an optional Data (Data?) type and I don't want to unwrap it and force unwraps are my most hated lines of code.

As we use JSON in our example, we need a JSONDecoder that is happily provided by Swift. Most decoders have a simple func decode(_:from:) function.

let decoder = JSONDecoder()

do {
    let deserializedData = try decoder.decode(Array<Product>.self, from: jsonData)
} catch {
    print("Decoding Error", error)
}

deserializedData is now from type Array<Product> or [Product] if you like the later one more.

If the decoding fails, you'll get an error explaining what went wrong and where. Like a key that is missing or was the wrong data type.

Recursive De-/Encoding

As I mentioned above, I left the currency value out of the model. Thats because I would like to implement this as an enum. You can add any other type to your model as they are recursively conformant to the same Codable protocols. This means that as our Product model is Codable, our enum has to be Codable as well.

Enums

For an enum to be En- and/or Decodable, it has to have a raw value if we don't want to write the logics for (de)serializing the object. We will come to that in a later, more advanced guide.

Lets add our currency enum:

struct Product: Codable {
    let title: String
    let price: Double
    let currency: Currency // added the property
}

enum Currency: String, Codable {
    case usDollar = "USD"
    case euro = "EUR"
}

This is it. We now have a currency property with an enum. One thing to notice: Any other value (like "GBP") will cause the deserialization to fail! And the most sad part is that it will also fail when you set currency to be an optional value.

Encode JSON

Encoding works just like decoding. Just instead of a JSONDecoder we use a JSONEncoder:

let encoder = JSONEncoder()
do {
    // deserializedData contains our Product array
    let serializedProductsData: Data = try encoder.encode(deserializedData)
    
    // Convert Data to String
    let serializedProductsString: String = String(data: serializedProductsData, encoding: .utf8)
} catch {
    print("Encoding Error", error)
}

At this stage we could also convert the data to a plist by using a PropertyListEncoder.

Please note, that you'll lose all properties on the encoding side that hadn't been parsed on the decoding side. If you use the example without the currency property, the encoding will drop that, too.

Wrap it up

I hope you got a little insight in how to work with Codables without using any Frameworks or fancy code. We will get into it and dive a little deeper in the next guides around this topic and how to handle stuff like the enum issue above and how to work with annoying APIs that you can streamline a little bit with Codables.

I will explain how to write custom (de)serialization for your models and how to solve some issues that can occur.