Swift 4 Decodable - 以enum为关键字典

时间:2017-06-23 15:34:02

标签: swift codable

我的数据结构有一个枚举作为键,我希望下面自动解码。这是一个错误还是一些配置问题?

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)
}

我得到的错误就是这个,似乎把dict与数组混淆了。

  

typeMismatch(Swift.Array,Swift.DecodingError.Context(codingPath:   [可选(__ lldb_expr_85.AStruct。(CodingKeys in   _0E2FD0A9B523101D0DCD67578F72D1DD).dictionary)],debugDescription:"预计解码数组但却找到了字典。"))

3 个答案:

答案 0 :(得分:22)

问题是Dictionary's Codable conformance目前只能正确处理StringInt个键。对于包含任何其他Key类型的字典(其中KeyEncodable / Decodable),使用 unkeyed 容器对其进行编码和解码(JSON数组)具有交替的键值。

因此在尝试解码JSON时:

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

AStruct"dictionary"键的值应该是一个数组。

所以,

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

会起作用,产生JSON:

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

然后将被解码为:

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

但是,我认为Dictionary Codable一致性能够正确处理任何CodingKey符合类型{{ {1}}(Key可以) - 因为它可以使用该密钥对带密钥的容器进行编码和解码(请{2}随意请求)。

直到实现(如果有的话),我们总是可以构建一个包装器类型来执行此操作:

AnEnum

然后像这样实现:

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)
        }
    }
}

(或只是拥有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) } } 类型的dictionary属性,并使用自动生成的CodableDictionary<AnEnum, String>一致性 - 然后只需说出Codable

现在我们可以按预期解码嵌套的JSON对象:

dictionary.decoded

虽然这一切都在说,但可以说,使用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"]) 作为关键字的字典实现的只是具有可选属性的enum(如果您期望的话)永远存在的给定值;使其成为非可选的。)

因此,您可能只希望模型看起来像:

struct

使用您当前的JSON可以正常工作:

struct BStruct : Codable {
    var enumValue: String?
}

struct AStruct: Codable {

    private enum CodingKeys : String, CodingKey {
        case bStruct = "dictionary"
    }

    let bStruct: BStruct
}

答案 1 :(得分:3)

为了解决您的问题,您可以使用以下两个Playground代码段中的一个。

#1。使用Decodable的{​​{1}}初始值设定项

init(from:)

用法:

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
    }

}

#2。使用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" */ 的{​​{1}}方法

KeyedDecodingContainerProtocol

用法:

decode(_:forKey:)

答案 2 :(得分:0)

根据Imanou的回答,并成为超级通用。这将转换任何RawRepresentable枚举键字典。可腐烂物品中不需要其他代码。

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
    }
}