Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 4 Decodable with keys not known until decoding time

How does the Swift 4 Decodable protocol cope with a dictionary containing a key whose name is not known until runtime? For example:

  [     {       "categoryName": "Trending",       "Trending": [         {           "category": "Trending",           "trailerPrice": "",           "isFavourit": null,           "isWatchlist": null         }       ]     },     {       "categoryName": "Comedy",       "Comedy": [         {           "category": "Comedy",           "trailerPrice": "",           "isFavourit": null,           "isWatchlist": null         }       ]     }   ] 

Here we have an array of dictionaries; the first has keys categoryName and Trending, while the second has keys categoryName and Comedy. The value of the categoryName key tells me the name of the second key. How do I express that using Decodable?

like image 459
matt Avatar asked Aug 09 '17 18:08

matt


2 Answers

The key is in how you define the CodingKeys property. While it's most commonly an enum it can be anything that conforms to the CodingKey protocol. And to make dynamic keys, you can call a static function:

struct Category: Decodable {     struct Detail: Decodable {         var category: String         var trailerPrice: String         var isFavorite: Bool?         var isWatchlist: Bool?     }      var name: String     var detail: Detail      private struct CodingKeys: CodingKey {         var intValue: Int?         var stringValue: String          init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }         init?(stringValue: String) { self.stringValue = stringValue }          static let name = CodingKeys.make(key: "categoryName")         static func make(key: String) -> CodingKeys {             return CodingKeys(stringValue: key)!         }     }      init(from coder: Decoder) throws {         let container = try coder.container(keyedBy: CodingKeys.self)         self.name = try container.decode(String.self, forKey: .name)         self.detail = try container.decode([Detail].self, forKey: .make(key: name)).first!     } } 

Usage:

let jsonData = """   [     {       "categoryName": "Trending",       "Trending": [         {           "category": "Trending",           "trailerPrice": "",           "isFavourite": null,           "isWatchlist": null         }       ]     },     {       "categoryName": "Comedy",       "Comedy": [         {           "category": "Comedy",           "trailerPrice": "",           "isFavourite": null,           "isWatchlist": null         }       ]     }   ] """.data(using: .utf8)!  let categories = try! JSONDecoder().decode([Category].self, from: jsonData) 

(I changed isFavourit in the JSON to isFavourite since I thought it was a mispelling. It's easy enough to adapt the code if that's not the case)

like image 173
Code Different Avatar answered Sep 23 '22 15:09

Code Different


You can write a custom struct that functions as a CodingKeys object, and initialize it with a string such that it extracts the key you specified:

private struct CK : CodingKey {     var stringValue: String     init?(stringValue: String) {         self.stringValue = stringValue     }     var intValue: Int?     init?(intValue: Int) {         return nil     } } 

Thus, once you know what the desired key is, you can say (in the init(from:) override:

let key = // whatever the key name turns out to be let con2 = try! decoder.container(keyedBy: CK.self) self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!) 

So what I ended up doing is making two containers from the decoder — one using the standard CodingKeys enum to extract the value of the "categoryName" key, and another using the CK struct to extract the value of the key whose name we just learned:

init(from decoder: Decoder) throws {     let con = try! decoder.container(keyedBy: CodingKeys.self)     self.categoryName = try! con.decode(String.self, forKey:.categoryName)     let key = self.categoryName     let con2 = try! decoder.container(keyedBy: CK.self)     self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!) } 

Here, then, is my entire Decodable struct:

struct ResponseData : Codable {     let categoryName : String     let unknown : [Inner]     struct Inner : Codable {         let category : String         let trailerPrice : String         let isFavourit : String?         let isWatchList : String?     }     private enum CodingKeys : String, CodingKey {         case categoryName     }     private struct CK : CodingKey {         var stringValue: String         init?(stringValue: String) {             self.stringValue = stringValue         }         var intValue: Int?         init?(intValue: Int) {             return nil         }     }     init(from decoder: Decoder) throws {         let con = try! decoder.container(keyedBy: CodingKeys.self)         self.categoryName = try! con.decode(String.self, forKey:.categoryName)         let key = self.categoryName         let con2 = try! decoder.container(keyedBy: CK.self)         self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)     } } 

And here's the test bed:

    let json = """       [         {           "categoryName": "Trending",           "Trending": [             {               "category": "Trending",               "trailerPrice": "",               "isFavourit": null,               "isWatchlist": null             }           ]         },         {           "categoryName": "Comedy",           "Comedy": [             {               "category": "Comedy",               "trailerPrice": "",               "isFavourit": null,               "isWatchlist": null             }           ]         }       ]     """     let myjson = try! JSONDecoder().decode(         [ResponseData].self,          from: json.data(using: .utf8)!)     print(myjson) 

And here's the output of the print statement, proving that we've populated our structs correctly:

[JustPlaying.ResponseData(     categoryName: "Trending",      unknown: [JustPlaying.ResponseData.Inner(         category: "Trending",          trailerPrice: "",          isFavourit: nil,          isWatchList: nil)]),   JustPlaying.ResponseData(     categoryName: "Comedy",      unknown: [JustPlaying.ResponseData.Inner(         category: "Comedy",          trailerPrice: "",          isFavourit: nil,          isWatchList: nil)]) ] 

Of course in real life we'd have some error-handling, no doubt!


EDIT Later I realized (in part thanks to CodeDifferent's answer) that I didn't need two containers; I can eliminate the CodingKeys enum, and my CK struct can do all the work! It is a general purpose key-maker:

init(from decoder: Decoder) throws {     let con = try! decoder.container(keyedBy: CK.self)     self.categoryName = try! con.decode(String.self, forKey:CK(stringValue:"categoryName")!)     let key = self.categoryName     self.unknown = try! con.decode([Inner].self, forKey: CK(stringValue:key)!) } 
like image 38
matt Avatar answered Sep 21 '22 15:09

matt