Extracting data from JSON array with swift Codable

2019-05-19 06:14发布

问题:

I have a JSON response like this:

I have currently designed my decodable struct to be as follows:

    struct PortfolioResponseModel: Decodable {
    var dataset: Dataset

    struct Dataset: Decodable {
        var data: Array<PortfolioData> //I cannot use [Any] here...

        struct PortfolioData: Decodable {
            //how to extract this data ?
        }
    }
   }

The question is, how do I extract the data inside the array, which can have a value Double or String.

Here is the sample string to make this work on playground:

   let myJSONArray =
   """
   {
   "dataset": {
   "data": [
    [
   "2018-01-19",
   181.29
   ],
   [
   "2018-01-18",
   179.8
   ],
   [
   "2018-01-17",
   177.6
   ],
   [
   "2018-01-16",
   178.39
   ]
   ]
   }
   }
   """

Extracting the data:

do {
    let details2: PortfolioResponseModel = try JSONDecoder().decode(PortfolioResponseModel.self, from: myJSONArray.data(using: .utf8)!)
    //print(details2) 
    //print(details2.dataset.data[0]) //somehow get "2018-01-19"

} catch {
    print(error)
}

回答1:

I cannot use [Any] here.

Never use Any when decoding JSON because usually you do know the type of the contents.

To decode an array you have to use an unkeyedContainer and decode the values in series

struct PortfolioResponseModel: Decodable {
    var dataset: Dataset

    struct Dataset: Decodable {
        var data: [PortfolioData]

        struct PortfolioData: Decodable {
            let date : String
            let value : Double

            init(from decoder: Decoder) throws {
                var container = try decoder.unkeyedContainer()
                date = try container.decode(String.self)
                value = try container.decode(Double.self)
            }
        }
    }
}

You can even decode the date strings as Date

struct PortfolioData: Decodable {
    let date : Date
    let value : Double

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        date = try container.decode(Date.self)
        value = try container.decode(Double.self)
    }
}

if you add a date formatter to the decoder

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let details2 = try decoder.decode(PortfolioResponseModel.self, from: Data(myJSONArray.utf8))


回答2:

To add to this, there is a very good example of complex JSON parsing with arrays in particular here. I hope this helps those who are trying to use Codeable with larger, more realistic JSON data.

The overview is this: Imagine you had the following JSON format:

{
"meta": {
    "page": 1,
    "total_pages": 4,
    "per_page": 10,
    "total_records": 38
},
"breweries": [
    {
        "id": 1234,
        "name": "Saint Arnold"
    },
    {
        "id": 52892,
        "name": "Buffalo Bayou"
    }
]

}

This is a common format with the array nested inside. You could create a struct that encapsulates the entire response, accommodating arrays for the "breweries" key, similar to what you were asking above:

struct PagedBreweries : Codable {
struct Meta : Codable {
    let page: Int
    let totalPages: Int
    let perPage: Int
    let totalRecords: Int
    enum CodingKeys : String, CodingKey {
        case page
        case totalPages = "total_pages"
        case perPage = "per_page"
        case totalRecords = "total_records"
    }
}

struct Brewery : Codable {
    let id: Int
    let name: String
}

let meta: Meta
let breweries: [Brewery]

}