Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 4 Decodable - Dictionary with enum as key

Tags:

swift

codable

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."))

like image 820
Chris Mitchelmore Avatar asked Jun 23 '17 15:06

Chris Mitchelmore


1 Answers

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"))) 
like image 175
Hamish Avatar answered Sep 28 '22 11:09

Hamish