Search code examples
jsonswiftjsondecoder

How do I support a variable JSON field of array(array,string) in Swift?


all.

I'm trying to create a struct to hold the contents of an API call that has a certain structure, but not sure how to write it.

Here's one result from the API where I'm having issues coding the receiving struct for:

"10E": {
  "baseSetSize": 264, 
  "block": "Guilds of Ravnica", 
  "boosterV3": [
    [
      "rare", 
      "mythic rare"
    ], 
    "uncommon", 
    "uncommon", 
    "uncommon", 
    "common", 
    "common", 
    "common", 
    "common", 
    "common", 
    "common", 
    "common", 
    "common", 
    "common", 
    "common", 
    "land", 
    "marketing"
  ], 
  "cards": [ ... ]
}

The API lists the 'boosterV3' key as having the format array(array, string). However, some the returned data contains an array of 16 strings, and at least one has an array of strings as it's first element.

I've tried using the following struct:

struct MtGSet : Codable {
    let name: String
    let baseSetSize: Int
    let block: String
    let boosterV3: [String]
    let cards: [MtGCard]
}

But running against the API with this generates this error:

[CodingKeys(stringValue: "boosterV3", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], 
debugDescription: "Expected to decode String but found an array instead.", underlyingError: nil))

Is it possible to write a data structure in Swift that would accept either 16 String values or 15 String values and 1 array of String for the boosterV3 key?

Thanks.


Solution

  • Try like the below. I am not testing this I just created by converting JSON to Swift. you can also do it by yourself, here it is https://app.quicktype.io/.

    
    struct MtGSet : Codable {
        let name: String
        let baseSetSize: Int
        let block: String
        let boosterV3: [BoosterV3] // user [] of BoosterV3 instead of string
        let cards: [MtGCard]
    }
    
    enum BoosterV3: Codable {
        case string(String)
        case stringArray([String])
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let x = try? container.decode([String].self) {
                self = .stringArray(x)
                return
            }
            if let x = try? container.decode(String.self) {
                self = .string(x)
                return
            }
            throw DecodingError.typeMismatch(BoosterV3.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for BoosterV3"))
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .string(let x):
                try container.encode(x)
            case .stringArray(let x):
                try container.encode(x)
            }
        }
    }