Swift 4 Decodable - Dictionary with enum as key

前端 未结 3 2075
我在风中等你
我在风中等你 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 06:48

    In order to solve your problem, you can use one of the two following Playground code snippets.


    #1. Using Decodable's init(from:) initializer

    import Foundation
    
    enum AnEnum: String, Codable {
        case enumValue
    }
    
    struct AStruct {
        enum CodingKeys: String, CodingKey {
            case dictionary
        }
        enum EnumKeys: String, CodingKey {
            case enumValue
        }
    
        let dictionary: [AnEnum: String]
    }
    
    extension AStruct: Decodable {
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let dictContainer = try container.nestedContainer(keyedBy: EnumKeys.self, forKey: .dictionary)
    
            var dictionary = [AnEnum: String]()
            for enumKey in dictContainer.allKeys {
                guard let anEnum = AnEnum(rawValue: enumKey.rawValue) else {
                    let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to an AnEnum object")
                    throw DecodingError.dataCorrupted(context)
                }
                let value = try dictContainer.decode(String.self, forKey: enumKey)
                dictionary[anEnum] = value
            }
            self.dictionary = dictionary
        }
    
    }
    

    Usage:

    let jsonString = """
    {
      "dictionary" : {
        "enumValue" : "someString"
      }
    }
    """
    
    let data = jsonString.data(using: String.Encoding.utf8)!
    let decoder = JSONDecoder()
    let aStruct = try! decoder.decode(AStruct.self, from: data)
    dump(aStruct)
    
    /*
     prints:
     ▿ __lldb_expr_148.AStruct
       ▿ dictionary: 1 key/value pair
         ▿ (2 elements)
           - key: __lldb_expr_148.AnEnum.enumValue
           - value: "someString"
     */
    

    #2. Using KeyedDecodingContainerProtocol's decode(_:forKey:) method

    import Foundation
    
    public enum AnEnum: String, Codable {
        case enumValue
    }
    
    struct AStruct: Decodable {
        enum CodingKeys: String, CodingKey {
            case dictionary
        }
    
        let dictionary: [AnEnum: String]
    }
    
    public extension KeyedDecodingContainer  {
    
        public func decode(_ type: [AnEnum: String].Type, forKey key: Key) throws -> [AnEnum: String] {
            let stringDictionary = try self.decode([String: String].self, forKey: key)
            var dictionary = [AnEnum: String]()
    
            for (key, value) in stringDictionary {
                guard let anEnum = AnEnum(rawValue: key) else {
                    let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to an AnEnum object")
                    throw DecodingError.dataCorrupted(context)
                }
                dictionary[anEnum] = value
            }
    
            return dictionary
        }
    
    }
    

    Usage:

    let jsonString = """
    {
      "dictionary" : {
        "enumValue" : "someString"
      }
    }
    """
    
    let data = jsonString.data(using: String.Encoding.utf8)!
    let decoder = JSONDecoder()
    let aStruct = try! decoder.decode(AStruct.self, from: data)
    dump(aStruct)
    
    /*
     prints:
     ▿ __lldb_expr_148.AStruct
       ▿ dictionary: 1 key/value pair
         ▿ (2 elements)
           - key: __lldb_expr_148.AnEnum.enumValue
           - value: "someString"
     */
    
    0 讨论(0)
  • 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<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")))
    
    0 讨论(0)
  • 2020-12-01 07:11

    Following from Imanou's answer, and going super generic. This will convert any RawRepresentable enum keyed dictionary. No further code required in the Decodable items.

    public extension KeyedDecodingContainer
    {
        func decode<K, V, R>(_ type: [K:V].Type, forKey key: Key) throws -> [K:V]
            where K: RawRepresentable, K: Decodable, K.RawValue == R,
                  V: Decodable,
                  R: Decodable, R: Hashable
        {
            let rawDictionary = try self.decode([R: V].self, forKey: key)
            var dictionary = [K: V]()
    
            for (key, value) in rawDictionary {
                guard let enumKey = K(rawValue: key) else {
                    throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath,
                         debugDescription: "Could not parse json key \(key) to a \(K.self) enum"))
                }
                
                dictionary[enumKey] = value
            }
    
            return dictionary
        }
    }
    
    0 讨论(0)
提交回复
热议问题