Search code examples
jsonswiftparsingcodablemoya

How to parse nested JSON arrays from object returned via Moya.Response query


I am trying to parse a JSON object that I have as a result from my network session (done through MoyaProvider). The returned JSON object has nested JSON arrays in it. It looks something like this:

Edit: Link to the json file is here. results.json

{
“resultCount” : 50,
“results” :
[
    {
        “data1”: 1
        “data2”: 2
    },
    {
        “data1”: 1
        “data2”: 2
    },
    {
        “data1”: 1
        “data2”: 2
    }
]
}

Now using Moya, I could get the data by using Moya.Response API like this:

let jsonObj = try response.mapJSON()

But I don't want to do that I want to map it to my Model struct. I have done something like this below. Which I have checked (through OPTION + MouseHover) that objMovie is of type [Movie]

let objMovie = try response.map(ITunesSearchResults<Movie>.self).results

I have followed a tutorial online using similar technique but I d don't understand why objMovie does not contain the return values from after executing the line above. I tried to do a

print(obj.< propertyofMovie >)

but nothing is showing on the console.

So what gives?

Here are some code snippets. Where ITunesSearchResults is:

struct ITunesSearchResults<T: Decodable>: Decodable {
    let results: [T]
}

And my Movie structure is this. It conforms to the key values found in the JSON nested array properties.

struct Movie: Codable
{
   let trackId: Int
   let trackName: String
   let trackGenre: String
   let trackPrice: Int?
   let longDescription: String

   init(trackId: Int, trackName: String, trackGenre: String, 
   trackPrice: 
   Int?, /*trackImage: Thumbnail,*/ longDescription: String)
   {
    self.trackId = trackId
    self.trackName = trackName
    self.trackGenre = trackGenre
    self.trackPrice = trackPrice ?? 0
    self.longDescription = longDescription
    //self.trackImage = trackImage //TODO: thumbnail: mapp url from json
   }

  private enum MovieCodingKeys: String, CodingKey
  {
    case trackId
    case trackName
    case trackGenre = "primaryGenreName"
    case trackPrice
    //case trackImage
    case longDescription
  }

  init(from decoder: Decoder) throws
  {
    let container = try decoder.container(keyedBy: MovieCodingKeys.self)

    trackId = try container.decode(Int.self, forKey: .trackId)
    trackName = try container.decode(String.self, forKey: .trackName)
    trackGenre = try container.decode(String.self, forKey: .trackGenre)
    trackPrice = try container.decode(Int?.self, forKey: .trackPrice)
    longDescription = try container.decode(String.self, forKey: .longDescription)
  }
}

Solution

  • First of all use the Data response of Moya as you do want to decode the JSON with Decodable for example

    let data = response.data
    

    Your Movie struct is too complicated, explicit CodingKeys and an init method is not needed at all, you get them for free. This is sufficient:

    struct ITunesSearchResults<T: Decodable>: Decodable {
        let results: [T]
    }
    
    struct Movie: Decodable
    {
        let trackId: Int
        let trackName: String
        let trackGenre: String?
        let trackPrice: Double?
        let longDescription: String
    }
    

    Note that trackPrice is Double and trackGenre and trackPrice are optional.

    Now decode simply

    do {
        let result = try JSONDecoder().decode(ITunesSearchResults<Movie>.self, from: data)
        print(result)
    } catch { print(error) }
    

    Note:

    Never use a syntax like

    try container.decode(Int?.self, forKey: .trackPrice)
    

    There is decodeIfPresent

    try container.decodeIfPresent(Int.self, forKey: .trackPrice)
    

    The huge benefit is it throws an error if the key is present but the type is wrong like in this concrete case.