Swift 4 Decodable - Dictionary with enum as key

前端 未结 3 2098
我在风中等你
我在风中等你 2020-12-01 06:27

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          


        
3条回答
  •  孤城傲影
    2020-12-01 07:11

    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 : 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 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")))
    

提交回复
热议问题