Omar Ebrahim
Omar Ebrahim

Reputation: 95

Parse decodable struct property as any data type

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?

Upvotes: 1

Views: 610

Answers (1)

Sweeper
Sweeper

Reputation: 271460

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)

Upvotes: 2

Related Questions