Search code examples
swiftswift4codabledecodable

Hot to decode JSON data that could and array or a single element in Swift?


I have a struct named Info which is decoded based on the data it receives. But sometimes, one of the values in data can either be a double or an array of double. How do I set up my struct for that?

struct Info: Decodable {
    let author: String
    let title: String
    let tags: [Tags]
    let price: [Double]
    enum Tags: String, Decodable {
        case nonfiction
        case biography
        case fiction
    }
}

Based on the url, I either get price as a double

{
    "author" : "Mark A",
    "title" : "The Great Deman",
    "tags" : [
      "nonfiction",
      "biography"
    ],
    "price" : "242"

}

or I get it as an array of doubles

{
    "author" : "Mark A",
    "title" : "The Great Deman",
    "tags" : [
      "nonfiction",
      "biography"
    ],
    "price" : [
    "242",
    "299",
    "335"
    ]

}

I want to setup my struct so that if I receive a double instead of an array of doubles, price should be decoded as an array of 1 double.


Solution

  • Your JSON actually is either a String or an array of Strings. So you need to create a custom decoder to decode and then convert them to Double:

    struct Info {
        let author, title: String
        let tags: [Tags]
        let price: [Double]
        enum Tags: String, Codable {
            case nonfiction, biography, fiction
        }
    }
    

    extension Info: Codable {
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            author = try container.decode(String.self, forKey: .author)
            title  = try container.decode(String.self, forKey: .title)
            tags = try container.decode([Tags].self, forKey: .tags)
            do {
                price = try [Double(container.decode(String.self, forKey: .price)) ?? .zero]
            } catch {
                price = try container.decode([String].self, forKey: .price).compactMap(Double.init)
            }
        }
    }
    

    Playground testing

    let infoData = Data("""
    {
        "author" : "Mark A",
        "title" : "The Great Deman",
        "tags" : [
          "nonfiction",
          "biography"
        ],
        "price" : "242"
    
    }
    """.utf8)
    do {
        let info = try JSONDecoder().decode(Info.self, from: infoData)
        print("price",info.price)  // "price [242.0]\n"
    } catch {
        print(error)
    }
    

    let infoData2 = Data("""
    {
        "author" : "Mark A",
        "title" : "The Great Deman",
        "tags" : [
          "nonfiction",
          "biography"
        ],
        "price" : [
        "242",
        "299",
        "335"
        ]
    
    }
    """.utf8)
    
    do {
        let info = try JSONDecoder().decode(Info.self, from: infoData2)
        print("price",info.price)  // "price [242.0, 299.0, 335.0]\n"
    } catch {
        print(error)
    }