1

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?

Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • What type do you want to decode the arrays as? `[AnyObject]` isn't that helpful, is it? How do you plan on using its contents, anyway? – Sweeper Feb 08 '23 at 09:26
  • I've updated the JSON in the question. Each object will have a `resource` key, and the array will be the same length and types in each position. So if I want `resource B`, I know the array is going to be date, int, int, string. I want to parse the array content as an object, or just parse individually by index. – Omar Ebrahim Feb 08 '23 at 09:30
  • Ah, that makes much more sense. – Sweeper Feb 08 '23 at 09:32
  • What about `headers`? I don't see that in your JSON. Do you need that for anything? If you don't or if that is also just determined by the "resource" key, then `ResultSet` could just be an enum with associated values, I think. – Sweeper Feb 08 '23 at 09:39
  • `headers` are always a string, apologies, I omitted it from the JSON – Omar Ebrahim Feb 08 '23 at 09:45

1 Answers1

2

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)
Sweeper
  • 213,210
  • 22
  • 193
  • 313