The two structs that I am using are Scoreboard and ResultSet
struct ResultSet: Codable {
var name: String
var headers: [String]
//var rowSet: [String]
}
struct Scoreboard: Codable {
var resultSets: [ResultSet]
}
The issue comes with the rowSet
property of ResultSet
, as this is an array of any type and length, so
[{
"resource": "resource A",
"rowSet": [
["A", 1, "test1"],
["B", 2, "test2"]
],
},
{
"resource": "resource B",
"rowSet": [
["2/28/2022", 1, 4, "loss"],
["3/28/2022", 2, 3, "win"]
],
}]
Parsing it as a string results in a parsing error. Setting the type to [AnyObject]
doesn't build as it doesn't conform to Decodable
How should this be parsed?
Since the "resource" key determine what the data in the "rowSet" key mean, I would model this as a enum with associated values.
First, create models for the two kinds of resources. Add decoding initialisers that allows them to be decoded from JSON arrays.
// I only implemented the Decodable side.
// The Encodable side should be trivial to do once you understand the idea
struct ResourceA: Decodable {
// not sure what these properties mean...
let property1: String
let property2: Int
let property3: String
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
property1 = try container.decode(String.self)
property2 = try container.decode(Int.self)
property3 = try container.decode(String.self)
}
}
struct ResourceB: Decodable {
let dateString: String
let score1: Int
let score2: Int
let result: String
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
dateString = try container.decode(String.self) // I'm a bit lazy - you can parse this to a Date on your own :)
score1 = try container.decode(Int.self)
score2 = try container.decode(Int.self)
result = try container.decode(String.self)
}
}
Then change ResultSet
to an enum
with cases corresponding to the types of resources. In the decoding initialiser, you first decode the "resource" key, and switch
on that to decide which kind of resource to decode for the "rowSet" key.
enum ResultSet: Decodable {
// if the headers can be computed from the resource type,
// you don't need it as an associated value - just add it as a computed property instead
case resourceA([ResourceA], headers: [String])
case resourceB([ResourceB], headers: [String])
enum CodingKeys: CodingKey {
case resource
case headers
case rowSet
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let resourceName = try container.decode(String.self, forKey: .resource)
let headers = try container.decode([String].self, forKey: .headers)
switch resourceName {
case "resource A":
self = .resourceA(try container.decode([ResourceA].self, forKey: .rowSet), headers: headers)
case "resource B":
self = .resourceB(try container.decode([ResourceB].self, forKey: .rowSet), headers: headers)
default:
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Unknown resource name \(resourceName)"))
}
}
}
Example usage:
let json = """
[{
"resource": "resource A",
"headers": [],
"rowSet": [
["A", 1, "test1"],
["B", 2, "test2"]
],
},
{
"resource": "resource B",
"headers": [],
"rowSet": [
["2/28/2022", 1, 4, "loss"],
["3/28/2022", 2, 3, "win"]
],
}]
""".data(using: .utf8)!
let decoded = try JSONDecoder().decode([ResultSet].self, from: json)