Reputation: 85
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!
Upvotes: 3
Views: 2739
Reputation: 21
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.
Upvotes: 2
Reputation: 285260
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)
}
}
}
Upvotes: 3