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 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) }
The error I get is this, seems to confuse the dict with an array.
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."))
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")))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With