Home > Enterprise >  Decoding JSON structures with minor differences into just one format
Decoding JSON structures with minor differences into just one format

Time:09-28

I have three API endpoints that result in the same final structure, however the full JSON structure received from the API is a bit different in each of them.

First JSON result

{
    "tracks": [{
            "name": "Never Gonna Give You Up"
     }]
}

Second JSON result

{
    "items": [{
            "name": "Never Gonna Give You Up"
     }]
}

Third JSON result

{
    "items": [{
        "track": {
            "name": "Never Gonna Give You Up"
        }
    }]
}

I want all of them to look like this

{
    "tracks": [{
        "name": "Never Gonna Give You Up"
    }]
}

For that I'm using three different structures:


First:

struct TopHitsTrackResponse: Decodable {
  var tracks: [Track]  
}

Second:

struct FavoritesTrackResponse: Decodable {
  var tracks: [Track]

  enum CodingKeys: String, CodingKey {
    case tracks = "items"
  }
}

And the third one is the code below.

What I've tried

I have successfully made the first and second JSON results look exactly equal to the wanted result. However, the third one is a bit more complicated for me. Here's what I've tried without success.

  struct NestedTrackResponse: Decodable {
    let tracks: [Track]

    enum CodingKeys: String, CodingKey {
      case tracks = "items"
    }

    enum TrackKeys: String, CodingKey {
      case track
    }

    init(from decoder: Decoder) throws {
      let outerContainer = try decoder.container(keyedBy: CodingKeys.self)
      let trackContainer = try outerContainer.nestedContainer(keyedBy: TrackKeys.self,
                                                              forKey: .tracks)

      self.tracks = try trackContainer.decode([Track].self, forKey: .track)
    }

    struct Track: Decodable {
      var name: String
    }
  }

I'm calling the API with this function

  AF.request(urlRequest)
    .validate()
    .responseDecodable(of: NestedTrackResponse.self) { response in
       // It's always resulting in fatal error
       guard let data = response.value else {
         fatalError("Error receiving tracks from API.")
       }
    }

  // - `AF` is Alamofire, but I've already tried not using it, 
  // and the error persists

  // - `urlRequest` is just the URL of the API and the API key, 
  // doesn't really matter for this problem

And getting this error

Expected to decode Dictionary<String, Any> but found an array instead.

CodePudding user response:

You have to write a custom initializer with if let expressions to distinguish the cases.

struct Response : Decodable {
    let tracks : [Track]
    
    private enum CodingKeys : String, CodingKey { case items, tracks }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let tracks = try? container.decode([Track].self, forKey: .items) {
            self.tracks = tracks
        } else if let tracks = try? container.decode([Track].self, forKey: .tracks) {
            self.tracks = tracks
        } else if let items = try? container.decode([Item].self, forKey: .items) {
            self.tracks = items.map(\.track)
        } else {
            throw DecodingError.dataCorruptedError(forKey: .items, in: container, debugDescription: "Unsupported JSON structure")
        }
    }
}

struct Item : Decodable {
    let track : Track
}

struct Track : Decodable {
    let name : String
}

CodePudding user response:

As I said in the other answer, recommending to stick to the JSON structure that you receive from the API, this will prevent many headaches in future, as well as having a well-defined networking layer.

struct Track: Decodable {
    var name: String
}

struct TopHitsTrackResponse: Decodable {
  var tracks: [Track]  
}

struct FavoritesTrackResponse: Decodable {
  var items: [Track]
}

// though you should name this by the API name,
// like with the other two
struct NestedTrackResponse: Decodable {
    var items: [Item]

    struct Item: Decodable {
        var track: Track
    }
}

Basically the above is your networking layer. Now, comes the business layer, which will try to extract the tracks from each kind of response.

You could implement it with protocols, but functions work as well (if not even better):

func tracks(from response: TopHitsTrackResponse) -> [Track] {
    response.tracks
}

func tracks(from response: FavoritesTrackResponse) -> [Track] {
    response.items
}

func tracks(from response: NestedTrackResponse) -> [Track] {
    response.items.map(\.track)
}

Then, in your API callbacks, you simply call tracks(from:), in order to extract the most wanted results.

  • Related