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