Swift 4 Decodable: Beyond The Basics 📦

One of the features that I was looking forward to this year WWDC was Codable, which is just a type alias of the Encodable and Decodable protocols.

I’ve just spent a whole week shifting my Swift projects from using custom JSON parsers to Decodable (while removing a lot of code! 🎉), this post showcases what I’ve learned along the way.

Basics

If you haven’t seen it already, I suggest you to watch the related WWDC session (the Codable part starts past 23 minutes).

In short: you can now convert a set of data from a JSON Object or Property List to an equivalent Struct or Class, basically without writing a single line of code.

Here’s an example:

import Foundation

struct Swifter: Decodable {
  let fullName: String
  let id: Int
  let twitter: URL
}

let json = """
{
 "fullName": "Federico Zanetello",
 "id": 123456,
 "twitter": "http://twitter.com/zntfdr"
}
""".data(using: .utf8)! // our data in native (JSON) format
let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // Decoding our data
print(myStruct) // decoded!!!!!

What the Compiler Does Without Telling Us

If we look at the Decodable documentation, we see that the protocol requires the implementation of a init(from: Decoder) method.

We didn’t implemented it in the Playground: why did it work?

It’s for the same reason why we don’t have to implement a Swifter initializer, but we can still go ahead and initialize our struct: the Swift compiler provides one for us! 🙌

Conforming to Decodable

All of the above is great and just works™ as long as all we need to parse is a subsets of primitives (strings, numbers, bools, etc) or other structures that conform to the Decodable protocol.

But what about parsing more “complex structures”? Well, in this case we have to do some work.

Implementing init(from: Decoder)

⚠️ This part might be a bit trickier to understand: everything will be clear with the examples below!

Before diving into our own implementation of this initializer, let’s take a look at the main players:

The Decoder

As the name implies, the Decoder transforms something into something else: in our case this means moving from a native format (e.g. JSON) into an in-memory representation.

We will focus on two of the Decoder’s functions:

  1. container<Key>(keyedBy: Key.Type)
  2. singleValueContainer()

In both cases, the Decoder returns a (Data) Container.

With the first function, the Decoder returns a keyed container, KeyedDecodingContainer: to reach the actual data, we must first tell the container which keys to look for (more on this later!).

The second function tells the decoder that there’s no key: the returned container, SingleValueDecodingContainer, is actually the data that we want!

The Containers

Thanks to our Decoder we’ve moved from a raw native format to a structure that we can play with (our containers). Time to extract our data! Let’s take a look at the two containers that we’ve just discovered:

KeyedDecodingContainer

In this case we know that our container is keyed, you can think of this container as a dictionary [Key: Any].

Different keys can hold different types of data: which is why the container offers several decode(Type:forKey:) methods.

This method is where the magic happens: by calling it, the container returns us our data’s value of the given type for the given key (examples below!).

Most importantly, the container offers the generic method decode<T>(T.Type, forKey: K) throws -> T where T: Decodable which means that any type, as long as it conforms to Decodable, can be used with this function! 🎉🎉

SingleValueDecodingContainer

Everything works as above, just without any keys.

Implementing our init(from: Decoder)

We’ve seen all the players that will help us go from data stored in our disk to data that we can use in our App: let’s put them all together!

Take the playground at the start of the article for example: instead of letting the compiler doing it for us, let’s implement our own init(from: Decoder).

Step 1: Choosing The Right Decoder

The example’s data is a JSON object, therefore we will use the Swift Library’s JSONDecoder.

let decoder = JSONDecoder()

⚠️ JSON and P-list encoders and decoders are embedded in the Swift Library: you can write your own coders to support different formats!

Step 2: Determining The Right Container

In our case the data is keyed:

{
 "fullName": "Federico Zanetello",
 "id": 123456,
 "twitter": "http://twitter.com/zntfdr"
}

To reach "Federico Zanetello" we must ask for the value of key "fullName", to reach 123456 we must ask for the valued of index "id", etc.

Therefore, we must use a KeyedDecodingContainer Container (by calling the Decoder’s method container<Key>(keyedBy: Key.Type)).

But before doing so, as requested by the method, we must declare our keys: Key is actually a protocol and the easiest way to implement it is by declaring our keys as an enum of type String:

enum MyStructKeys: String, CodingKey {
  case fullName = "fullName"
  case id = "id"
  case twitter = "twitter"
}

Note: you don’t have to write = “…” in each case: but for clarity’s sake I’ve chosen to write it.

Now that we have our keys set up, we can go on and create our container:

let container = try decoder.container(keyedBy: MyStructKeys.self)

Step 3: Extracting Our Data

Finally, we must convert the container’s data into something that we can use in our app:

let fullName: String = try container.decode(String.self, forKey: .fullName)
let id: Int = try container.decode(Int.self, forKey: .id)
let twitter: URL = try container.decode(URL.self, forKey: .twitter)

Step 4: Initializing our Struct/Class

We can use the default Swifter initializer:

let myStruct = Swifter(fullName: fullName, id: id, twitter: twitter)

Voila! We’ve just implemented Decodable all by ourselves! 👏🏻👏🏻 Here’s the final playground:

//: Playground - noun: a place where people can play
import Foundation

struct Swifter {
  let fullName: String
  let id: Int
  let twitter: URL
  
  init(fullName: String, id: Int, twitter: URL) { // default struct initializer
    self.fullName = fullName
    self.id = id
    self.twitter = twitter
  }
}

extension Swifter: Decodable {
  enum MyStructKeys: String, CodingKey { // declaring our keys 
    case fullName = "fullName"
    case id = "id"
    case twitter = "twitter"
  }
  
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: MyStructKeys.self) // defining our (keyed) container
    let fullName: String = try container.decode(String.self, forKey: .fullName) // extracting the data
    let id: Int = try container.decode(Int.self, forKey: .id) // extracting the data
    let twitter: URL = try container.decode(URL.self, forKey: .twitter) // extracting the data
    
    self.init(fullName: fullName, id: id, twitter: twitter) // initializing our struct
  }
}

let json = """
{
 "fullName": "Federico Zanetello",
 "id": 123456,
 "twitter": "http://twitter.com/zntfdr"
}
""".data(using: .utf8)! // our native (JSON) data
let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // decoding our data
print(myStruct) // decoded!

Going further (More Playgrounds!)

Now that our Swifter struct conforms to Decodable, any other struct/class/etc that contains such data can automatically decode Swifter for free. For example:

Arrays

import Foundation

struct Swifter: Decodable {
  let fullName: String
  let id: Int
  let twitter: URL
}

let json = """
[{
 "fullName": "Federico Zanetello",
 "id": 123456,
 "twitter": "http://twitter.com/zntfdr"
},{
 "fullName": "Federico Zanetello",
 "id": 123456,
 "twitter": "http://twitter.com/zntfdr"
},{
 "fullName": "Federico Zanetello",
 "id": 123456,
 "twitter": "http://twitter.com/zntfdr"
}]
""".data(using: .utf8)! // our data in native format
let myStructArray = try JSONDecoder().decode([Swifter].self, from: json)

myStructArray.forEach { print($0) } // decoded!!!!!

Dictionaries

import Foundation

struct Swifter: Codable {
  let fullName: String
  let id: Int
  let twitter: URL
}

let json = """
{
  "one": {
    "fullName": "Federico Zanetello",
    "id": 123456,
    "twitter": "http://twitter.com/zntfdr"
  },
  "two": {
    "fullName": "Federico Zanetello",
    "id": 123456,
    "twitter": "http://twitter.com/zntfdr"
  },
  "three": {
    "fullName": "Federico Zanetello",
    "id": 123456,
    "twitter": "http://twitter.com/zntfdr"
  }
}
""".data(using: .utf8)! // our data in native format
let myStructDictionary = try JSONDecoder().decode([String: Swifter].self, from: json)

myStructDictionary.forEach { print("\($0.key): \($0.value)") } // decoded!!!!!

Enums

import Foundation

struct Swifter: Decodable {
  let fullName: String
  let id: Int
  let twitter: URL
}

enum SwifterOrBool: Decodable {
  case swifter(Swifter)
  case bool(Bool)
}

extension SwifterOrBool: Decodable {
  enum CodingKeys: String, CodingKey {
    case swifter, bool
  }
  
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    if let swifter = try container.decodeIfPresent(Swifter.self, forKey: .swifter) {
      self = .swifter(swifter)
    } else {
      self = .bool(try container.decode(Bool.self, forKey: .bool))
    }
  }
}

let json = """
[{
"swifter": {
   "fullName": "Federico Zanetello",
   "id": 123456,
   "twitter": "http://twitter.com/zntfdr"
  }
},
{ "bool": true },
{ "bool": false },
{
"swifter": {
   "fullName": "Federico Zanetello",
   "id": 123456,
   "twitter": "http://twitter.com/zntfdr"
  }
}]
""".data(using: .utf8)! // our native (JSON) data
let myEnumArray = try JSONDecoder().decode([SwifterOrBool].self, from: json) // decoding our data
  
myEnumArray.forEach { print($0) } // decoded!

More Complex Structs

import Foundation

struct Swifter: Decodable {
  let fullName: String
  let id: Int
  let twitter: URL
}

struct MoreComplexStruct: Decodable {
  let swifter: Swifter
  let lovesSwift: Bool
}

let json = """
{
  "swifter": {
    "fullName": "Federico Zanetello",
    "id": 123456,
    "twitter": "http://twitter.com/zntfdr"
  },
  "lovesSwift": true
}
""".data(using: .utf8)! // our data in native format
let myMoreComplexStruct = try JSONDecoder().decode(MoreComplexStruct.self, from: json)

print(myMoreComplexStruct.swifter) // decoded!!!!!

And so on!

Before Departing

In all probability, during your first Decodable implementations, something will go wrong: maybe it’s a key mismatch, maybe it’s a type mismatch, etc.

To detect all of these errors early, I suggest you to use Swift Playgrounds with error handling as much as possible:

do {
  let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // do your decoding here
} catch {
  print(error) // any decoding error will be printed here!
}

You can go even deeper by splitting different types of Decoding Errors.

Even on the first Xcode 9 beta, the error messages are clear and on point 💯.

Conclusions

I was really looking forward to this new Swift 4 feature and I’m very happy with its implementation. Time to drop all those custom JSON parsers!

That’s all for today! Happy Decoding!

⭑⭑⭑⭑⭑

Further Reading

Explore Swift

Browse all