(Not a book by JK Rowling, tho I’m sure many people would purchase and read Harry Potter and the Codable Protocol)
I’m working on a project where I need to write code to convert the following JSON
into a structure:
{ "en" : { "stringUnit" : {
"state" : "translated",
"value" : "Cat" } },
"fr" : { "stringUnit" : {
"state" : "translated",
"value" : "Chat" } },
"ja" : { "stringUnit" : {
"state" : "translated",
"value" : "ねこ" } }
}
To accomplish this, I created the following model objects:
typealias LocalizationsDict = [String: Localization]
struct Localization: Codable, Equatable {
let stringUnit: StringUnit
}
struct StringUnit: Codable, Equatable {
let state: String
let value: String
}
The code to decode a LocalizationsDict
looked like this:
let languages: LocalizationsDict = try JSONDecoder().decode(LocalizationsDict.self, from: data)
Everything was working wonderfully. But then I got greedy. (queue the jump scare music.) I wanted to see if the keys could be an enum
, instead of a String
. I felt this would make the code safer, and would mean languages could be types using autocomplete. (The jury may still be out on whether necessity or laziness is truly the mother of invention.)
Here was the code needed to create the Language enum
:
enum Language: String, Codable {
case en = "en"
case fr = "fr"
case ja = "ja"
}
Unfortunately…in order for a Swift dictionary to conform to Codable
, its key type must be either String
or Int
. For a Swift dictionary with any other type of key, decode will assume the JSON
will be represented as an array, where the first item is the first key, the second item is the first value, third item is the second key, etc.
Thank you Apple Dev forums, yet again. https://developer.apple.com/forums/thread/747665
This would mean the JSON
would need to be stored like this:
[ "en", { "stringUnit" : {
"state" : "translated",
"value" : "Cat" } },
"fr", { "stringUnit" : {
"state" : "translated",
"value" : "Chat" } },
"ja", { "stringUnit" : {
"state" : "translated",
"value" : "ねこ" } }
}
Sadly, in this situation, the JSON
was being generated by the metaphorical ‘somebody else’ and I didn’t have the option to change the format of my incoming JSON
.
Instead I added a step when decoding this type of Dictionary. Here’s a code snippet:
typealias LocalizationsDict = [Language: Localization]
typealias LocalizationsDict2 = [String: Localization]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let stringKeyedLocalizations = try container.decode(LocalizationsDict2.self, forKey: .localizations)
var enumKeyedLocalizations: LocalizationsDict = [:]
for (key, value) in stringKeyedLocalizations {
if let enumKey = Language(rawValue: key) {
enumKeyedLocalizations[enumKey] = value
}
}
self.localizations = enumKeyedLocalizations
}
The details of what’s happening are as follows:
- Decode the
JSON
to a[String: Localization]
dictionary - Create an empty
[Language: Localization]
dictionary - Iterate through the
[String: Localization]
dictionary - For each entry, verify it’s possible to create a valid enum value from the language string. (eg map
"en"
toLanguage.en
- For each entry store the value in the
[Language: Localization]
dictionary, using theenum
key generated in the previous step.
This works, but I thought this seemed like an interesting shortcoming in Swift.