Swift 4 Decodable - 以枚举为键的字典
Swift 4 Decodable - Dictionary with enum as key
我的数据结构有一个枚举作为键,我希望下面的代码能够自动解码。这是错误还是某些配置问题?
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(codingPath:
[Optional(__lldb_expr_85.AStruct.(CodingKeys in
_0E2FD0A9B523101D0DCD67578F72D1DD).dictionary)], debugDescription: "Expected to decode Array but found a dictionary instead."))
在 Swift 5.6 (Xcode 13.3) SE-0320 CodingKeyRepresentable 中已经实施,解决了这个问题。
它添加了对符合 RawRepresentable
和 Int
和 String
原始值的枚举键入的字典的隐式支持。
问题是 Dictionary
's Codable
conformance 目前只能正确处理 String
和 Int
键。对于具有任何其他 Key
类型的字典(其中 Key
是 Encodable
/Decodable
),它使用 unkeyed[=73] 进行编码和解码=] 具有交替键值的容器(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
可以)——因为它可以用那个密钥编码和解码到一个带密钥的容器中(请随时向 file a bug 提出要求)。
在实现之前(如果有的话),我们总是可以构建一个包装器类型来做到这一点:
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: [=14=], value: try container.decode(Value.self, forKey: [=14=]))
}
)
}
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)
}
}
(或者只使用 CodableDictionary<AnEnum, String>
类型的 dictionary
属性 并使用自动生成的 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")))
为了解决您的问题,您可以使用以下两个 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"
*/
根据 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
}
}
我的数据结构有一个枚举作为键,我希望下面的代码能够自动解码。这是错误还是某些配置问题?
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(codingPath: [Optional(__lldb_expr_85.AStruct.(CodingKeys in _0E2FD0A9B523101D0DCD67578F72D1DD).dictionary)], debugDescription: "Expected to decode Array but found a dictionary instead."))
在 Swift 5.6 (Xcode 13.3) SE-0320 CodingKeyRepresentable 中已经实施,解决了这个问题。
它添加了对符合 RawRepresentable
和 Int
和 String
原始值的枚举键入的字典的隐式支持。
问题是 Dictionary
's Codable
conformance 目前只能正确处理 String
和 Int
键。对于具有任何其他 Key
类型的字典(其中 Key
是 Encodable
/Decodable
),它使用 unkeyed[=73] 进行编码和解码=] 具有交替键值的容器(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
可以)——因为它可以用那个密钥编码和解码到一个带密钥的容器中(请随时向 file a bug 提出要求)。
在实现之前(如果有的话),我们总是可以构建一个包装器类型来做到这一点:
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: [=14=], value: try container.decode(Value.self, forKey: [=14=]))
}
)
}
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)
}
}
(或者只使用 CodableDictionary<AnEnum, String>
类型的 dictionary
属性 并使用自动生成的 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")))
为了解决您的问题,您可以使用以下两个 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"
*/
根据 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
}
}