swiftcodable

Swift 4 Decodable - Dictionary with enum as key


My data structure has an enum as a key, I would expect the below to decode automatically. Is this a bug or some configuration issue?

import Foundation

enum AnEnum: String, Codable {
  case enumValue
}

struct AStruct: Codable {
  let dictionary: [AnEnum: String]
}

let jsonDict = ["dictionary": ["enumValue": "someString"]]
let data = try! JSONSerialization.data(withJSONObject: jsonDict,     options: .prettyPrinted)
let decoder = JSONDecoder()
do {
  try decoder.decode(AStruct.self, from: data)
} catch {
  print(error)
}

The error I get is this, seems to confuse the dict with an array.

typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [Optional(__lldb_expr_85.AStruct.(CodingKeys in _0E2FD0A9B523101D0DCD67578F72D1DD).dictionary)], debugDescription: "Expected to decode Array but found a dictionary instead."))


Solution

  • From Swift 5.6

    Swift Proposal [SE-0320] allows non String/Int (e.g. a enum) as key of a dictionary.

    Conform your enum to the CodingKeyRepresentable protocol.

    e.g.

    enum AnEnum: String, Codable, CodingKeyRepresentable {
      case enumValue
    }
    

    Before Swift 5.6 (original answer)

    The problem is that Dictionary's Codable conformance can currently only properly handle String and Int keys. For a dictionary with any other Key type (where that Key is Encodable/Decodable), it is encoded and decoded with an unkeyed container (JSON array) with alternating key values.

    Therefore when attempting to decode the JSON:

    {"dictionary": {"enumValue": "someString"}}
    

    into AStruct, the value for the "dictionary" key is expected to be an array.

    So,

    let jsonDict = ["dictionary": ["enumValue", "someString"]]
    

    would work, yielding the JSON:

    {"dictionary": ["enumValue", "someString"]}
    

    which would then be decoded into:

    AStruct(dictionary: [AnEnum.enumValue: "someString"])
    

    However, really I think that Dictionary's Codable conformance should be able to properly deal with any CodingKey conforming type as its Key (which AnEnum can be) – as it can just encode and decode into a keyed container with that key (feel free to file a bug requesting for this).

    Until implemented (if at all), we could always build a wrapper type to do this:

    struct CodableDictionary<Key : Hashable, Value : Codable> : Codable where Key : CodingKey {
        
        let decoded: [Key: Value]
        
        init(_ decoded: [Key: Value]) {
            self.decoded = decoded
        }
        
        init(from decoder: Decoder) throws {
            
            let container = try decoder.container(keyedBy: Key.self)
            
            decoded = Dictionary(uniqueKeysWithValues:
                try container.allKeys.lazy.map {
                    (key: $0, value: try container.decode(Value.self, forKey: $0))
                }
            )
        }
        
        func encode(to encoder: Encoder) throws {
            
            var container = encoder.container(keyedBy: Key.self)
            
            for (key, value) in decoded {
                try container.encode(value, forKey: key)
            }
        }
    }
    

    and then implement like so:

    enum AnEnum : String, CodingKey {
        case enumValue
    }
    
    struct AStruct: Codable {
        
        let dictionary: [AnEnum: String]
        
        private enum CodingKeys : CodingKey {
            case dictionary
        }
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            dictionary = try container.decode(CodableDictionary.self, forKey: .dictionary).decoded
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(CodableDictionary(dictionary), forKey: .dictionary)
        }
    }
    

    (or just have the dictionary property of type CodableDictionary<AnEnum, String> and use the auto-generated Codable conformance – then just speak in terms of dictionary.decoded)

    Now we can decode the nested JSON object as expected:

    let data = """
    {"dictionary": {"enumValue": "someString"}}
    """.data(using: .utf8)!
     
    let decoder = JSONDecoder()
    do {
        let result = try decoder.decode(AStruct.self, from: data)
        print(result)
    } catch {
        print(error)
    }
    
    // AStruct(dictionary: [AnEnum.enumValue: "someString"])
    

    Although that all being said, it could be argued that all you're achieving with a dictionary with an enum as a key is just a struct with optional properties (and if you expect a given value to always be there; make it non-optional).

    Therefore you may just want your model to look like:

    struct BStruct : Codable {
        var enumValue: String?
    }
    
    struct AStruct: Codable {
        
        private enum CodingKeys : String, CodingKey {
            case bStruct = "dictionary"
        }
        
        let bStruct: BStruct
    }
    

    Which would work just fine with your current JSON:

    let data = """
    {"dictionary": {"enumValue": "someString"}}
    """.data(using: .utf8)!
     
    let decoder = JSONDecoder()
    do {
        let result = try decoder.decode(AStruct.self, from: data)
        print(result)
    } catch {
        print(error)
    }
    
    // AStruct(bStruct: BStruct(enumValue: Optional("someString")))