Swift 4 Decodable with keys not known until decoding time - json

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

Here's what I eventually came up for this json:
let json = """
{
"BTC_BCN":{
"last":"0.00000057",
"percentChange":"0.03636363",
"baseVolume":"47.08463318"
},
"BTC_BELA":{
"last":"0.00001281",
"percentChange":"0.07376362",
"baseVolume":"5.46595029"
}
}
""".data(using: .utf8)!
We make such a structure:
struct Pair {
let name: String
let details: Details
struct Details: Codable {
let last, percentChange, baseVolume: String
}
}
then decode:
if let pairsDictionary = try? JSONDecoder().decode([String: Pair.Details].self, from: json) {
var pairs: [Pair] = []
for (name, details) in pairsDictionary {
let pair = Pair(name: name, details: details)
pairs.append(pair)
}
print(pairs)
}
It is also possible to call not pair.details.baseVolume, but pair.baseVolume:
struct Pair {
......
var baseVolume: String { return details.baseVolume }
......
Or write custom init:
struct Pair {
.....
let baseVolume: String
init(name: String, details: Details) {
self.baseVolume = details.baseVolume
......

Related

Parsing issue : APIError: keyNotFound key CodingKeys(stringValue: "data", intValue: nil)

Unable to parse using codable structs for following json
{
"data": {
"listDeviceStateTable": {
"items": [
{
"Data": "{'state': -1, 'remainSec': 0}",
"PK": "DEVICE#144b584b-xxxx-xxxx-xxxx-1e584bdb1e8c",
"SK": "Station1"
},
{
"Data": "{'state': -1, 'remainSec': 0}",
"PK": "DEVICE#144b584b-xxxx-xxxx-xxxx-1e584bdb1e8c",
"SK": "Station2"
}
]
}
}
}
Error :
APIError: keyNotFound key CodingKeys(stringValue: "data", intValue:
nil) Caused by: keyNotFound(CodingKeys(stringValue: "data", intValue:
nil), Swift.DecodingError.Context(codingPath: [], debugDescription:
"No value associated with key CodingKeys(stringValue: "data",
intValue: nil) ("data").", underlyingError: nil)))
Model:
//MARK: DeviceState
struct DeviceState:Codable {
let data: DataClass
}
// MARK: - DataClass
struct DataClass: Codable {
let listDeviceStateTable: ListDeviceStateTable
}
// MARK: - ListDeviceStateTable
struct ListDeviceStateTable: Codable {
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let data, pk, sk: String
enum CodingKeys: String, CodingKey {
case data = "Data"
case pk = "PK"
case sk = "SK"
}
}
works well for me. This is the test code I used to show how to decode the json data into your structs. Of course Item->data is here decoded as a String, as per the json you show.
struct ContentView: View {
#State var devState: DeviceState?
var body: some View {
Group {
if let devFirst = devState?.data.listDeviceStateTable.items.first {
Text("pk: \(devFirst.pk)")
}
}
.onAppear {
let json = """
{
"data": {
"listDeviceStateTable": {
"items": [
{
"Data": "{'state': -1, 'remainSec': 0}",
"PK": "DEVICE#144b584b-xxxx-xxxx-xxxx-1e584bdb1e8c",
"SK": "Station1"
},
{
"Data": "{'state': -1, 'remainSec': 0}",
"PK": "DEVICE#144b584b-xxxx-xxxx-xxxx-1e584bdb1e8c",
"SK": "Station2"
}
]
}
}
}
"""
let data = json.data(using: .utf8)!
do {
devState = try JSONDecoder().decode(DeviceState.self, from: data)
print(devState)
} catch {
print("\(error)")
}
}
}
}
struct DeviceState:Codable {
let data: DataClass
}
// MARK: - DataClass
struct DataClass: Codable {
let listDeviceStateTable: ListDeviceStateTable
}
// MARK: - ListDeviceStateTable
struct ListDeviceStateTable: Codable {
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let data, pk, sk: String
enum CodingKeys: String, CodingKey {
case data = "Data"
case pk = "PK"
case sk = "SK"
}
}
to decode Data into something else than a String, eg a ItemData, you need to remove the double quotes,
and replace the single quote with double quotes (in the value not the key).
eg. "Data": "{'state': -1, 'remainSec': 0}", to "Data": {"state": -1, "remainSec": 0},
and use the following Item struct and decoding code:
// MARK: - Item
struct Item: Codable {
let data: ItemData
let pk, sk: String
enum CodingKeys: String, CodingKey {
case data = "Data"
case pk = "PK"
case sk = "SK"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
pk = try container.decode(String.self, forKey: .pk)
sk = try container.decode(String.self, forKey: .sk)
do {
let theString = try container.decode(String.self, forKey: .data)
let json = theString.replacingOccurrences(of: "\"", with: "").replacingOccurrences(of: "'", with: "\"")
data = try JSONDecoder().decode(ItemData.self, from: json.data(using: .utf8)!)
} catch DecodingError.typeMismatch {
data = ItemData(state: 0, remainSec: 0) // <-- todo
}
}
}
struct ItemData: Codable {
let state, remainSec: Int
}
You need one more struct to decode data inside Item like this
struct Item: Codable {
let data: ItemData
let pk, sk: String
enum CodingKeys: String, CodingKey {
case data = "Data"
case pk = "PK"
case sk = "SK"
}
}
struct ItemData: Codable {
let state, remainSec: Int
}

Parse using Codable in Swift

Need to parse this JSON in such a way that i should be able to access the benefits associated to each plan inside the "enabled" key within "items" node as below:
let items = Items[0].plans.main[0].enabled[0].text
{
"MyData": {
"data": {
"benefits": {
"B1": {
"text": "Text1"
},
"B2": {
"text": "Text2"
},
"B3": {
"text": "text3"
}
}
},
"items": [
{
"plans": {
"main": [
{
"name": "plan1",
"enabled": [
"B1",
"B2"
],
"disabled": [
"B2",
"B3"
]
}
]
}
}
]
}
}
I have tried as below to achieve but seems like this is not working
class Main: Codable {
var name: String?
var enabled: [String]?
var disabled: [String]?
enum CodingKeys: String, CodingKey {
case name = "name"
case enabled = "enabled"
case disabled = "disabled"
}
class MyData: Codable {
var benefits: [String: Benefit]?
enum CodingKeys: String, CodingKey {
case benefits = "benefits"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let propertyContainer = try container.nestedContainer(keyedBy: CustomDynamicKey.self, forKey: .benefits)
self.benefits = propertyContainer.decodeValues()
}
class Benefit: Codable {
var text: String?
enum CodingKeys: String, CodingKey {
case text = "text"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
text = try container.decode(String.self, forKey: .text)
}
}
struct CustomDynamicKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
extension KeyedDecodingContainer where Key == DynamicKey {
func decodeValues() -> [String : Benefit] {
var dict = [String : Benefit]()
for key in allKeys {
if let md = try? decode(Benefit.self, forKey: key) {
dict[key.stringValue] = md
} else {
print("unsupported key")
}
}
return dict
}
}
I tried to parse the models individually. However, i am able to access the models separately but i need to map the corresponding the benefit with the respective plan at the time of parsing JSON itself inside the required init() methods using Manual parsing.
One way could be to use JSONDecoder alongside JSONSerialization. Codables are a bit uncomfortable to use in such situations, and you can make var benefits: [String: Benefit]? optional (which you already have) and remove it from the CodingKeys enum. Then, use JSONSerialization to get the benefits field filled.
See This

In Swift: How do I decode this JSON (which has variable keys)?

There is an API that supplies JSON data that I would like to use. I've given a summary of the JSON below. At the top level, the key to each record is a unique ID that matches the ID in the record itself. These keys are integers in quotes (starting at 1, unsorted and probably not contiguous).
Reading the JSON isn't a problem. What is the Codable "Response" struct required to receive the data?
if let response = try? JSONDecoder().decode(Response.self, from: data)
The JSON
{
"2546": {
"id": "2546",
"title": "Divis and the Black Mountain"
},
"1": {
"id": "1",
"title": "A la Ronde"
},
"2": {
"id": "2",
"title": "Aberconwy House"
}
}
I had this once also, looks like whoever created this endpoint doesn't really understand how JSON works...
try this out and then just return response.values so you have a list of items
struct Item: Codable {
let id, title: String
}
typealias Response = [String: Item]
Use a more dynamic version of CodingKey. You can read more about it here: https://benscheirman.com/2017/06/swift-json/
Check the section "Dynamic Coding Keys"
The Codable type struct Response should be,
struct Response: Decodable {
let id: String
let title: String
}
Now, parse the json data using [String:Response] instead of just Response like so,
do {
let response = try JSONDecoder().decode([String:Response].self, from: data)
print(response) //["1": Response(id: "1", title: "A la Ronde"), "2546": Response(id: "2546", title: "Divis and the Black Mountain"), "2": Response(id: "2", title: "Aberconwy House")]
} catch {
print(error)
}
You should implement a custom CodingKey, something like that:
struct MyResponse {
struct MyResponseItemKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
static let id = MyResponseItemKey(stringValue: "id")!
static let title = MyResponseItemKey(stringValue: "title")!
}
struct MyResponseItem {
let id: String
let subItem: MyResponseSubItem
}
struct MyResponseSubItem {
let id: String
let title: String
}
let responseItems: [MyResponseItem]
}
Not sure if the key of each item and the value of id are always equal, that's why there are 2 IDs in MyResponse.
And, of course, MyResponse should conform to Codable:
extension MyResponse: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MyResponseItemKey.self)
responseItems = try container.allKeys.map { key in
let containerForKey = try container.nestedContainer(keyedBy: MyResponseItemKey.self, forKey: key)
let id = try containerForKey.decode(String.self, forKey: .id)
let title = try containerForKey.decode(String.self, forKey: .title)
return MyResponseItem(id: key.stringValue, subItem: MyResponseSubItem(id: id, title: title))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: MyResponseItemKey.self)
for responseItem in responseItems {
if let key = MyResponseItemKey(stringValue: responseItem.id) {
var subItemContainer = container.nestedContainer(keyedBy: MyResponseItemKey.self, forKey: key)
try subItemContainer.encode(responseItem.subItem.id, forKey: .id)
try subItemContainer.encode(responseItem.subItem.title, forKey: .title)
}
}
}
}
This is how you can use MyResponse:
let jsonString = """
{
"2546": {
"id": "2546",
"title": "Divis and the Black Mountain"
},
"1": {
"id": "1",
"title": "A la Ronde"
},
"2": {
"id": "2",
"title": "Aberconwy House"
}
}
"""
if let dataForJSON = jsonString.data(using: .utf8),
let jsonDecoded = try? JSONDecoder().decode(MyResponse.self, from: dataForJSON) {
print(jsonDecoded.responseItems.first ?? "")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let dataFromJSON = try? encoder.encode(jsonDecoded) {
let jsonEncoded = String(data: dataFromJSON, encoding: .utf8)
print(jsonEncoded ?? "")
}
}

Swift Codable: Decode different array of items with same root objects

I'm currently trying to decode JSON which looks like this:
{
"result": {
"success": true,
"items": [
{
"timeEntryID": "1",
"start": "1519558200",
"end": "1519563600",
"customerName": "Test-Customer",
"projectName": "Test-Project",
"description": "Entry 1",
},
{
"timeEntryID": "2",
"start": "1519558200",
"end": "1519563600",
"customerName": "Test-Customer",
"projectName": "Test-Project",
"description": "Entry 2",
}
],
"total": "2"
},
"id": "1"
}
The decoding process for this specific type of JSON is pretty simple. I just need something like this:
struct ResponseKeys: Decodable {
let result: ResultKeys
struct ResultKeys: Decodable {
let success: Bool
let items: [Item]
}
}
Now the problem I'm facing is that every response of the server has the same structure as the above JSON but with different item types. So sometimes it is let items: [Item] but it could also be let items: [User] if I make a call to the User endpoint.
Because it would be an unnecessary duplication of code if I would write the above swift code for every endpoint with just the modification of the items array, I created a custom decoder:
enum KimaiAPIResponseKeys: String, CodingKey {
case result
enum KimaiResultKeys: String, CodingKey {
case success
case items
}
}
struct Activity: Codable {
let id: Int
let description: String?
let customerName: String
let projectName: String
let startDateTime: Date
let endDateTime: Date
enum CodingKeys: String, CodingKey {
case id = "timeEntryID"
case description
case customerName
case projectName
case startDateTime = "start"
case endDateTime = "end"
}
}
extension Activity {
init(from decoder: Decoder) throws {
let resultContainer = try decoder.container(keyedBy: KimaiAPIResponseKeys.self)
let itemsContainer = try resultContainer.nestedContainer(keyedBy: KimaiAPIResponseKeys.KimaiResultKeys.self, forKey: .result)
let activityContainer = try itemsContainer.nestedContainer(keyedBy: Activity.CodingKeys.self, forKey: .items)
id = Int(try activityContainer.decode(String.self, forKey: .id))!
description = try activityContainer.decodeIfPresent(String.self, forKey: .description)
customerName = try activityContainer.decode(String.self, forKey: .customerName)
projectName = try activityContainer.decode(String.self, forKey: .projectName)
startDateTime = Date(timeIntervalSince1970: Double(try activityContainer.decode(String.self, forKey: .startDateTime))!)
endDateTime = Date(timeIntervalSince1970: Double(try activityContainer.decode(String.self, forKey: .endDateTime))!)
}
}
The decoder works perfectly if "items" does only contain a single object and not an array:
{
"result": {
"success": true,
"items":
{
"timeEntryID": "2",
"start": "1519558200",
"end": "1519563600",
"customerName": "Test-Customer",
"projectName": "Test-Project",
"description": "Entry 2",
},
"total": "2"
},
"id": "1"
}
If items is an array I get the following error:
typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [__lldb_expr_151.KimaiAPIResponseKeys.result], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil))
I just cannot figure out how to modify my decoder to work with an array of items. I created a Playground file with the working and not working version of the JSON. Please take a look and try it out: Decodable.playground
Thank you for your help!
My suggestion is to decode the dictionary/dictionaries for items separately
struct Item : Decodable {
enum CodingKeys: String, CodingKey {
case id = "timeEntryID"
case description, customerName, projectName
case startDateTime = "start"
case endDateTime = "end"
}
let id: Int
let startDateTime: Date
let endDateTime: Date
let customerName: String
let projectName: String
let description: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = Int(try container.decode(String.self, forKey: .id))!
description = try container.decodeIfPresent(String.self, forKey: .description)
customerName = try container.decode(String.self, forKey: .customerName)
projectName = try container.decode(String.self, forKey: .projectName)
startDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .startDateTime))!)
endDateTime = Date(timeIntervalSince1970: Double(try container.decode(String.self, forKey: .endDateTime))!)
}
}
And in Activity use a conditional initializer, it provides it's own do catch block. First it tries to decode a single item and assigns the single item as array to the property. If it fails it decodes an array.
enum KimaiAPIResponseKeys: String, CodingKey {
case result, id
enum KimaiResultKeys: String, CodingKey {
case success
case items
}
}
struct Activity: Decodable {
let id: String
let items: [Item]
}
extension Activity {
init(from decoder: Decoder) throws {
let rootContainer = try decoder.container(keyedBy: KimaiAPIResponseKeys.self)
id = try rootContainer.decode(String.self, forKey: .id)
let resultContainer = try rootContainer.nestedContainer(keyedBy: KimaiAPIResponseKeys.KimaiResultKeys.self, forKey: .result)
do {
let item = try resultContainer.decode(Item.self, forKey: .items)
items = [item]
} catch {
items = try resultContainer.decode([Item].self, forKey: .items)
}
}
}
You Can Use Generics, It's a neat way to deal with this situation.
struct MainClass<T: Codable>: Codable {
let result: Result<T>
let id: String
}
struct Result <T: Codable>: Codable {
let success: Bool
let items: [T]
let total: String
}
and here you will get the items
let data = Data()
let decoder = JSONDecoder()
let modelObjet = try! decoder.decode(MainClass<User>.self, from: data)
let users = modelObjet.result.items
In my opinion, Generics is the best way to handle the duplication of code like this situations.

decoding dynamic keys and values from JSON

I'm trying to decode JSON where I have dynamic keys and value types. I've tried to follow the question and answer here and can get my dynamic keys but then seem unable to get the values as well as they type is always different. I would ideally like to be able to have the 'parameters' property as a dictionary but I'm unsure how to go about this. My code is below.
let jsonString = """
{
"type": "some type",
"message": "some message",
"parameters": {
"randomKey": "some value",
"anotherKey": 99,
"padlock": "another string",
"fakeKey": true
}
}
"""
struct MyStruct : Decodable {
let type:String!
let message:String!
var parameters:DynamicParameters!
struct DynamicParameters: Decodable {
var keys:[String]!
private struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "" }
init?(stringValue: String) { self.stringValue = stringValue }
}
init(from decoder: Decoder) throws {
self.keys = [String]()
let container = try decoder.container(keyedBy: CodingKeys.self)
for key in container.allKeys {
self.keys.append(key.stringValue)
}
}
}
}
if let jsonData = jsonString.data(using: .utf8) {
do {
let list = try JSONDecoder().decode(MyStruct.self, from: jsonData)
print (list)
/// prints MyStruct(type: some type, message: some message, parameters: DynamicParameters(keys: ["anotherKey", "randomKey", "padlock", "fakeKey"]))
} catch {
print(error)
}
}