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!