Scary Sharks and Custom CodingKeys

I had to handle a fun little challenge with Codable and unorthodox JSON recently (as you do).

Apple’s Codable API have been around for a while, and it’s an example of the best kind of API: it makes the easy things easy, and the hard things possible.

For example, let’s say I’d like to load in some JSON data about sharks in movies:

[
    {
	"type": "Great White Shark",
	"movie": "Jaws",
        "length": 15
    },
    {
        "type": "Megalodon",
        "movie": "The Meg",
        "length": 50
    },
    {
        "type": "Mako Shark",
        "movie": "Deep Blue Sea",
        "length": 14
    }
]

Make a struct with the same fields, let loose your decoder, and you’re done!

struct MovieShark: Codable {
    let type: String
    let movie: String
    let length: Float
}

let sharks = try JSONDecoder().decode([MovieShark].self, from: data)

(Note: there are a few more steps to get running code, but you get the gist.)

If you need to fiddle with the keys a bit, say, because your data uses shark_type instead of just type, you add a CodingKeys entry:

    enum CodingKeys: String, CodingKey {
        case type = "shark_type"
        case movie
        case length
    }

I’ve tried (admittedly not that hard) to figure how how CodingKeys works.

  • How can defining a nested enum of a particular name cause this behavior?
  • How does the Swift enum type automatically conform to CodingKey?

Googling didn’t turn up any details on this. And, I mean, it doesn’t matter, right?

Well, sometimes it does.

The challenge I was facing was that my data wasn’t in the format shown above, but rather in this format:

[
    {
        "Great White Shark" : {
            "movie": "Jaws",
            "length": 15
	}
    },
    {
        "Megalodon": {
            "movie": "The Meg",
            "length": 50
        }
    },
    {
        "Mako Shark": {
            "movie": "Deep Blue Sea",
            "length": 14
        }
    }
]

The simple Codable approach can’t handle top-level dynamic keys like this. My question was, can I use Codable to do this at all?

And the answer is yes.

Turns out, CodingKeys doesn’t have to be an enum.

You can implement this protocol with a struct that has all its requirements: a string property and initializer, and an integer property and initializer.

Once you’ve done that, of course, you don’t have the static keys you still need, so I stashed those in a separate, unrelated enum called OtherCodingKeys.

struct MovieShark {
    let type: String
    let movie: String
    let length: Float
    
    struct CodingKeys: CodingKey {
        var stringValue: String
        init(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    enum OtherCodingKeys {
        case movie
        case length
    }
}

You’ll notice MovieShark no longer declares itself as implementing Codable here. That’s because I need to implement custom versions of Encodable and Decodable separately.

First, Encodable:

private struct MovieSharkContents: Codable {
    let movie: String
    let length: Float
}

extension MovieShark: Encodable {
    func encode(to coder: Encoder) throws {
        var container = coder.container(keyedBy: CodingKeys.self)
        try container.encode(MovieSharkContents(movie: movie, length: length), forKey: CodingKeys(stringValue: type))
    }
}

There are two steps:

  • Get the top-level container of the encoder with coder.container(keyedBy: CodingKeys.self). This is the standard way to start a custom encoding.
  • Specify a dynamic key by using the CodingKeys string initializer, CodingKeys(stringValue: type). You can’t just specify a random string, because that won’t be of the correct type.

Note, because the values of the subsequent dictionary are heterogenous, and Swift can’t serialize a [String: Any] type, I had to make an intermediate type, MovieSharkContents, to represent it.

Next, Decodable:

enum MovieSharkError: Error {
    case unableToDecode
}

extension MovieShark: Decodable {
     init(from coder: Decoder) throws {
         let container = try coder.container(keyedBy: CodingKeys.self)
         for key in container.allKeys {
             type = key.stringValue
             let contents = try container.decode(MovieSharkContents.self, forKey: key)
             movie = contents.movie
             length = contents.length
             return
         }
         throw MovieSharkError.unableToDecode
     }
}

Here, since the top-level key has an unknown name, I iterate through all the top-level keys, and pick the first.

I transfer that top-level key to the type property, and the dictionary values inside it to the other properties of MovieShark. If I don’t find any top-level key at all, I throw an exception.

In this way, I can keep a straightforward MovieShark struct with all the properties I expect, but also handle both loading and saving its custom JSON.

I figured out how to do this, by the way, from the very helpful Flight School Guide to Swift Codable. I still don’t get all the Swift magic behind CodingKeys, but I know a little more about how to use it!