Topics

#318: Codable Enums 📠

Topics

In Bites #316 and #317, we began looking at Swift's new Codable (Encodable & Decodable) protocols. Today we'll continue by learning more about how Swift Enums work can work with these protocols. Let's get started.

First off, lets try a basic example. Here's an enum:

enum SpaceshipKind {
  case transport
  case freighter
  case fighter
}

If we simply do this:

enum SpaceshipKind : Codable {

We'll get an error: Type 'SpaceshipKind' does not conform to protocol 'Decodable'.

We can get around this by making our enum a "raw" value type like a String:

enum SpaceshipKind : String, Codable {

Nice. Since Strings are Codable, this works great. Same goes for Int and other basic Codable types.

This is a fine solution for simple use cases, but things get a little (alright, a lot) more involved when one or more of our enum cases has associated values.

Here's an example:

public enum ContentKind : Codable {
  case app(String)
  case movie(Int)
}

If we compile now, we get that same does not conform... error.

Let's fix that by extending our enum. First, since we'll be taking on more of the encoding and decoding logic ourselves, we'll need our own Swift Error type to throw when things go wrong.

extension ContentKind {
  enum CodingError: Error { case decoding(String) }

Then, we'll need to tell Swift the keys we'll be using to encode or decode our data. We'll use another enum for this, this one adopting the built-in CodingKey protocol:

extension ContentKind {
  enum CodingError: Error { case decoding(String) }
  enum CodableKeys: String, CodingKey { case app, movie }

At this point we simply have to implement two more functions:

init(from decoder: Decoder) throws
func encode(to encoder: Encoder) throws

First we want to add the ability to decode a new ContentKind enum from some encoded data, then we want to
enable encoding a ContentKind into some data.

Let's look at the implementation of decoding all at once for clarity:

init(from decoder: Decoder) throws {
  let values = try decoder.container(keyedBy: CodableKeys.self)

  if let bundleID = try? values.decode(String.self, forKey: .app) {
    self = .app(bundleID)
    return
  }

  if let storeID = try? values.decode(Int.self, forKey: .movie) {
    self = .movie(storeID)
    return
  }

  throw CodingError.decoding("Decoding Failed. \(dump(values))")
}

First we pull all the values out of the decoder's container.

Then we check for each of our two possible cases, configuring ourselves and returning if successful. Finally, if we didn't return successfully and reach the end, we throw one of those CodingErrors we defined earlier.

Neat.

Last but not least, lets implement encoding:

func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodableKeys.self)

  switch self {
  case let .app(bundleID):
    try container.encode(bundleID, forKey: .app)
  case let .movie(storeID):
    try container.encode(storeID, forKey: .movie)
  }
}

This time we grab the encoder's container up front, then simply switch on ourselves to see which value to encode.

Now our associated value enum can be encoded and decoded all we want. Nice!

Pro Tip: Writing all of this by hand can get extremely tedious for anything more than a few simple cases. Try something like Sourcery (Bites #292, #294, and #295) to generate this more boilerplate-ish Codable implementation.

That's all for now, have a question or idea for a Bite? Send it to hello@littlebitesofcocoa.com.