4

I have a JSON response like this:

enter image description here

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)
}

2 Answers 2

8

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))
Sign up to request clarification or add additional context in comments.

3 Comments

That works !! Thanks. Though I still don't understand how that init(from decoder: ) really works... Also, can this solution be scaled-up, as in, if the inner arrays were to contain lets say 5 items (3 String and 2 Double), can the init func be updated to handle this as well ?
init(from decoder: ) is a custom initializer where you can provide your own pattern to decode the passed object. unkeyedContainer is the only way to decode nested arrays. Of course the solution can be scaled-up, just add properties and decoding lines accordingly. The values are decoded in the order of appearance.
"The values are decoded in the order of appearance..." Cool, thanks.
-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]
}

Comments

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.