0

I am trying to parse a JSON API where I'm trying to extract the figures for the key value "data". As you can see these figures are nested inside two arrays where the second array doesn't have a key value to reference. How do I do this?

{
  "dataset": {
    "id": 9789340,
    "name": "DCC share price (DCC), Currency GBX",
    "description": "Stock Prices for Dcc Share Price (dcc), Currency Gbx from the London Stock Exchange.<br><br>Currency: GBX",
    "start_date": "2006-03-16",
    "end_date": "2017-11-22",
    "column_names": [
      "Date",
      "Price",
      "High",
      "Low",
      "Volume",
      "Last Close",
      "Change",
      "Var%"
    ],
    "data": [
      [
        "2017-11-22",
        7060.0,
        7185.0,
        7045.0,
        156444.0,
        7060.0,
        -95.0,
        -1.33
      ],
      [
        "2017-11-21",
        7155.0,
        7210.0,
        7130.0,
        189002.0,
        7155.0,
        -30.0,
        -0.42
      ]
    ]
  }
}

So far I've done this.

struct Dataset: Decodable {
    let name: String
    let description: String
    let column_names: [ColumnNames]
    let data: [StockData]
}

struct ColumnNames: Decodable {
    // What happens here??
}

struct StockData: Decodable {
    // What happens here??
}

guard let url = URL(string : jsonUrlString) else { return }

URLSession.shared.dataTask(with: url) { (data, response, err) in

    guard let jsonData = data else {
        return
    }

    do {
        let dataSet = try JSONDecoder().decode(Dataset.self, from: jsonData)
        let dataArray = dataset.data
        dump(dataArray)
        for stockDataArray in dataArray {
            for stockItems in stockDataArray
            dump(stockItems)
        }
    }
}

As you can see I don't know how to decode "StockData" because JSON data is an array of arrays. If I have answer to this then hopefully I will be able to resolve "ColumnNames" should be parsed. It also isn't a dictionary with key values to parse.

4
  • 2
    That's pretty horrible JSON. The crucial point is that the value types in data are different (String and Double). So you have to decode it manually. For column_names you don't need a separate type. It's simply [String]. And without the date string the nested data array would be simply [[Double]]. Commented Nov 26, 2017 at 21:20
  • I agree, horrible JSON. Thanks for your help with column_names. It's a pity this won't work for data [[Any]] or [[String?]] Commented Nov 26, 2017 at 21:50
  • Actually it's a pity that the operator of the web service sends that weird CSV style rather than regular dictionaries. I recommend to use traditional JSONSerialization and merge fields and values. Commented Nov 26, 2017 at 21:56
  • I presume the web service did this to condense the data. I agree JSONSerialization is the best way to straighten this out. I was hoping to avoid this step because ultimately I plan to map this data to a persistent store with Core Data. Commented Nov 26, 2017 at 22:09

1 Answer 1

1

This is a solution using the powerful customization capabilities of JSONDecoder

It creates dictionaries of merging column_names and the data arrays.

Here is your JSON

let jsonString = """
{
    "dataset": {
        "id": 9789340,
        "name": "DCC share price (DCC), Currency GBX",
        "description": "Stock Prices for Dcc Share Price (dcc), Currency Gbx from the London Stock Exchange.<br><br>Currency: GBX",
        "start_date": "2006-03-16",
        "end_date": "2017-11-22",
        "column_names": ["Date", "Price", "High", "Low", "Volume", "Last Close", "Change", "Var%"],
        "data": [["2017-11-22", 7060.0, 7185.0, 7045.0, 156444.0, 7060.0, -95.0, -1.33],
            ["2017-11-21", 7155.0, 7210.0, 7130.0, 189002.0, 7155.0, -30.0, -0.42]]
    }
}
"""

and two structs, one for the root object and one for Dataset, in this example only id and name are decoded, data will contain the merged array of dictionaries

struct Root : Decodable {
    let dataset : Dataset
}

struct Dataset : Decodable {

    private enum CodingKeys : String, CodingKey {
        case id, name, columnNames = "column_names", data
    }

    let id : Int
    let name : String

    var data = [[String:Any]]()

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        let columnNames = try container.decode([String].self, forKey: .columnNames)

        var outerContainer = try container.nestedUnkeyedContainer(forKey: .data)
        while !outerContainer.isAtEnd {
            var columnIndex = 0
            var dataSet = [String:Any]()
            var innerContainer = try outerContainer.nestedUnkeyedContainer()
            let date = try innerContainer.decode(String.self)
            var key = columnNames[columnIndex]
            dataSet[key] = date
            columnIndex += 1
            while !innerContainer.isAtEnd {
                let value = try innerContainer.decode(Double.self)
                key = columnNames[columnIndex]
                dataSet[key] = value
                columnIndex += 1
            }
            data.append(dataSet)
        }

    }
}

Now decode the stuff

let data = Data(jsonString.utf8)
do {
    let decoder = JSONDecoder()
    let root = try decoder.decode(Root.self, from: data)
    print(root.dataset)
} catch {
    print("error: ", error)
}
Sign up to request clarification or add additional context in comments.

3 Comments

That's awesome. Thank you. I'm getting a DecodingError. I think it's because the data for Double is sometimes a null value. This happens at a later point in the JSON data not shown in my snippet. (stringValue: "Index 2", intValue: Optional(2))], debugDescription: "Expected Double but found null instead.", underlyingError: nil))
Then you have to add an extra do - catch block in the while !innerContainer.isAtEnd loop to handle the error and assign a default value or just skip the key. But I'd look for a better web service ...
In the end I opted to use decodeIfPresent. So let value = try innerContainer.decodeIfPresent(Double.self) catches the null values. It seemed easier to implement than do - catch

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.