Categories
Software

Swift Dictionaries and the Codable Protocol

(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:

  1. Decode the JSON to a [String: Localization] dictionary
  2. Create an empty [Language: Localization] dictionary
  3. Iterate through the [String: Localization] dictionary
  4. For each entry, verify it’s possible to create a valid enum value from the language string. (eg map "en" to Language.en
  5. For each entry store the value in the [Language: Localization] dictionary, using the enum key generated in the previous step.

This works, but I thought this seemed like an interesting shortcoming in Swift.

Leave a Reply

Your email address will not be published. Required fields are marked *