Search code examples
swiftdecodable

What is wrong with my enum decoding in Swift?


I just tried this:

let test = "{ \"de\": \"Bisasam\", \"en\": \"Bulbasaur\" }"
let data = test.data(using: .utf8)!
do {
    let result = try JSONDecoder().decode([String: String].self, from: data)
    print("") // breakpoint here works
} catch {
    print("")
}

This works fine, however when I try to do this instead:

enum Lg: String, CaseIterable, Decodable {
    case deutsch = "de"
    case english = "en"
}

let test = "{ \"de\": \"Bisasam\", \"en\": \"Bulbasaur\" }"
let data = test.data(using: .utf8)!
do {
    let result = try JSONDecoder().decode([Lg: String].self, from: data)
    print("")
} catch {
    print("") // breakpoint here
}

With this error:

▿ DecodingError ▿ typeMismatch : 2 elements

  • .0 : Swift.Array ▿ .1 : Context
    • codingPath : 0 elements
    • debugDescription : "Expected to decode Array but found a dictionary instead."
    • underlyingError : nil

What am I doing wrong?

Thanks for your help


Solution

  • When you use a non-String non-Int type as the dictionary key, Dictionary's Decodable implementation expects an array of key value pairs, i.e.:

    ["de", "foo", "en": "bar"]
    

    This decodes to [.deutsch: "foo", .english: "bar"].

    JSON keys can only be strings after all, and the Decodable implementation doesn't know how to convert a random Decodable value into a string. It could have been made to check for RawRepresentable with RawValue == String, but it wasn't.

    One thing that it does check for though, is CodingKeyRepresentable, if you just conform Lg to that, then you can decode a JSON dictionary correctly.

    Conforming a type to CodingKeyRepresentable lets you opt in to encoding and decoding Dictionary values keyed by the conforming type to and from a keyed container, rather than encoding and decoding the dictionary as an unkeyed container of alternating key-value pairs.

    extension Lg: CodingKeyRepresentable, CodingKey {
        init?<T>(codingKey: T) where T : CodingKey {
            self.init(rawValue: codingKey.stringValue)
        }
        
        var codingKey: CodingKey {
            self
        }
    }
    

    Note that I also conformed it to CodingKey to make it easier to write the codingKey property required by CodingKeyRepresentable.