Swift 4 Decodable - 以枚举为键的字典

问题描述 投票:0回答:6

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

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

我得到的错误是这样的,似乎将字典与数组混淆了。

typeMismatch(Swift.Array,Swift.DecodingError.Context(编码路径: [可选(__lldb_expr_85.AStruct。(编码键在 _0E2FD0A9B523101D0DCD67578F72D1DD).dictionary)],debugDescription:“预期解码数组,但找到了字典。”))

swift codable
6个回答
53
投票

从 Swift 5.6 开始

Swift 提案 [SE-0320] 允许非 String/Int(例如枚举)作为字典的键。

使您的枚举符合

CodingKeyRepresentable
协议。

例如

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

Swift 5.6 之前 (原答案)

问题是

Dictionary
Codable
一致性
目前只能正确处理
String
Int
键。对于任何其他
Key
类型的字典(其中
Key
Encodable
/
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
符合类型作为其
Key
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
一致性 – 然后就用
dictionary.decoded
来说话)

现在我们可以按预期解码嵌套的 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(dictionary: [AnEnum.enumValue: "someString"])

尽管说了这么多,但可以说,使用以

enum
作为键的字典所实现的一切只是带有可选属性的
struct
(如果您期望给定值始终存在;使其成为非可选)。

因此,您可能只想让您的模型看起来像:

struct BStruct : Codable {
    var enumValue: String?
}

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

这对于您当前的 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")))

6
投票

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


#1。使用
Decodable
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
    }

}

用途:

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。使用
KeyedDecodingContainerProtocol
decode(_:forKey:)
方法

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
    }

}

用途:

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"
 */

6
投票

在 Swift 5.6 (Xcode 13.3) SE-0320 CodingKeyRepresentable 已实现,解决了该问题。

它添加了对由符合

RawRepresentable
且具有
Int
String
原始值的枚举键入的字典的隐式支持。


4
投票

Swift 提案 [SE-0320] 现在允许我们使用非

String/Int
(例如枚举)作为字典的键。

要实现这一点,类型只需符合

CodingKeyRepresentable
协议即可。

请参阅下面的示例:

enum Device: String, Codable, CodingKeyRepresentable {
   case iphone
   case mac
   case watch
}

var deviceCollection = [Device: [String]]()

// encoding and decoding will work exactly the same as String/Int
let data = try JSONEncoder().encode(deviceCollection)

let content = try JSONDecoder().decode(data, from: [Device: [String]].self)


1
投票

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

0
投票

按照 Giles 的回答,这里有相同的想法,但方向相反,用于编码

public extension KeyedEncodingContainer {
    mutating func encode<K, V, R>(_ value: [K: V], forKey key: Key) throws
        where K: RawRepresentable, K: Encodable, K.RawValue == R,
              V: Encodable,
              R: Encodable, R: Hashable {
        try self.encode(
            Dictionary(uniqueKeysWithValues: value.map { ($0.key.rawValue, $0.value) }),
            forKey: key
        )
    }

    mutating func encodeIfPresent<K, V, R>(_ value: [K: V]?, forKey key: Key) throws
        where K: RawRepresentable, K: Encodable, K.RawValue == R,
              V: Encodable,
              R: Encodable, R: Hashable {
        if let value = value {
            try self.encode(value, forKey: key)
        }
    }
}
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.