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.
[
{
"type": "Great White Shark",
"movie": "Jaws",
"length": 15
},
{
"type": "Megalodon",
"movie": "The Meg",
"length": 50
},
{
"type": "Mako Shark",
"movie": "Deep Blue Sea",
"length": 14
}
]
For example, let’s say I’d like to load in some JSON data about sharks in movies:
struct MovieShark: Codable {
let type: String
let movie: String
let length: Float
}
let sharks = try JSONDecoder().decode([MovieShark].self, from: data)
Make a struct with the same fields, let loose your decoder, and you’re done!
(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
CodingKeysstring 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!





