How to decode the JSON response from MusicKit Search Suggestion

Hello everyone,

I am trying to understand how to decode the JSON response returned by the suggestions/top results endpoint in MusicKit

As you can see the response returns suggestions, which has two different types, Albums and Songs within the same 'suggestions' array. How can I decode the response even if there are different types using a single struct?

{
  "results" : {
    "suggestions" : [
      {
        "content" : {
          "attributes" : {
            "url" : "https:\/\/music.apple.com\/us\/artist\/megan-thee-stallion\/1258989914",
            "name" : "Megan Thee Stallion",
            "genreNames" : [
              "Hip-Hop\/Rap"
            ]
          },
          "id" : "1258989914",
          "relationships" : {
            "albums" : {
              "data" : [
                {
                  "href" : "\/v1\/catalog\/us\/albums\/1537889223",
                  "type" : "albums",
                  "id" : "1537889223"
                }
              ],
              "next" : "\/v1\/catalog\/us\/artists\/1258989914\/albums?offset=25",
              "href" : "\/v1\/catalog\/us\/artists\/1258989914\/albums"
            }
          },
          "href" : "\/v1\/catalog\/us\/artists\/1258989914",
          "type" : "artists"
        },
        "kind" : "topResults"
      },
      {
        "content" : {
          "href" : "\/v1\/catalog\/us\/artists\/991187319",
          "attributes" : {
            "genreNames" : [
              "Hip-Hop\/Rap"
            ],
            "url" : "https:\/\/music.apple.com\/us\/artist\/moneybagg-yo\/991187319",
            "name" : "Moneybagg Yo"
          },
          "id" : "991187319",
          "type" : "artists",
          "relationships" : {
            "albums" : {
              "href" : "\/v1\/catalog\/us\/artists\/991187319\/albums",
              "data" : [
                {
                  "id" : "1550876571",
                  "href" : "\/v1\/catalog\/us\/albums\/1550876571",
                  "type" : "albums"
                }
              ],
              "next" : "\/v1\/catalog\/us\/artists\/991187319\/albums?offset=25"
            }
          }
        },
        "kind" : "topResults"
      }
    ]
  }
}
Answered by Frameworks Engineer in 681648022

Hello @ashinthetray,

Here's how I would suggest you go about this. First, define an enum with two cases, one containing an Album, and the other containing a Song.

enum MySearchSuggestionItem {
    case album(Album)
    case song(Song)
}

Then, make this type conform to Decodable with a small custom initializer that defers to the respective initializers of MusicKit's own types, based on the value of the type property of the resource:

extension MySearchSuggestionItem: Decodable {
    enum CodingKeys: CodingKey {
        case type
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let type = try values.decode(String.self, forKey: .type)
        switch type {
            case "albums":
                let album = try Album(from: decoder)
                self = .album(album)
            case "songs":
                let song = try Song(from: decoder)
                self = .song(song)
            default:
                let decodingErrorContext = DecodingError.Context(
                    codingPath: decoder.codingPath, 
                    debugDescription: "Unexpected type \"\(type)\" encountered for MySearchSuggestionItem."
                )
                throw DecodingError.typeMismatch(MySearchSuggestionItem.self, decodingErrorContext)
        }
    }
}

This is the key to solving your problem.

Optionally, you can add a custom method to describe this object in a more succinct way:

extension MySearchSuggestionItem: CustomStringConvertible {
    var description: String {
        let description: String
        switch self {
            case .album(let album):
                description = "MySearchSuggestionItem.album(\(album))"
            case .song(let song):
                description = "MySearchSuggestionItem.song(\(song))"
        }
        return description
    }
}

Then you can use this type in your larger structure to decode the search suggestions response:

struct MyCatalogSearchSuggestionsResponse: Decodable {
    struct Results: Decodable {
        struct TopResultSuggestion: Decodable {
            let content: MySearchSuggestionItem
        }
        
        let suggestions: [TopResultSuggestion]
    }
    
    let results: Results
}

Then here's sample code tying this all together:

let countryCode = try await MusicDataRequest.currentCountryCode

var searchSuggestionsURLComponents = URLComponents()
searchSuggestionsURLComponents.scheme = "https"
searchSuggestionsURLComponents.host = "api.music.apple.com"
searchSuggestionsURLComponents.path = "/v1/catalog/\(countryCode)/search/suggestions"
searchSuggestionsURLComponents.queryItems = [
    URLQueryItem(name: "term", value: "discovery"), 
    URLQueryItem(name: "kinds", value: "topResults"), 
    URLQueryItem(name: "types", value: "albums,songs"), 
]
let searchSuggestionsURL = searchSuggestionsURLComponents.url!

let searchSuggestionsDataRequest = MusicDataRequest(urlRequest: URLRequest(url: searchSuggestionsURL))
let searchSuggestionsDataResponse = try await searchSuggestionsDataRequest.response()

let decoder = JSONDecoder()
let searchSuggestionsResponse = try decoder.decode(MyCatalogSearchSuggestionsResponse.self, from: searchSuggestionsDataResponse.data)

for topResultSuggestions in searchSuggestionsResponse.results.suggestions {
    print("\(topResultSuggestions.content)")
}

And here's the output produced by this code:

MySearchSuggestionItem.album(Album(id: "697194953", title: "Discovery", artistName: "Daft Punk"))
MySearchSuggestionItem.song(Song(id: "1440808397", title: "The Bad Touch", artistName: "Bloodhound Gang"))
MySearchSuggestionItem.song(Song(id: "1290141098", title: "Discovery Channel", artistName: "Lika Morgan"))
MySearchSuggestionItem.song(Song(id: "1440767972", title: "The Bad Touch", artistName: "Bloodhound Gang"))
MySearchSuggestionItem.album(Album(id: "1434140802", title: "Discovery", artistName: "Rivers & Robots"))

I hope this helps.

Best regards,

Accepted Answer

Hello @ashinthetray,

Here's how I would suggest you go about this. First, define an enum with two cases, one containing an Album, and the other containing a Song.

enum MySearchSuggestionItem {
    case album(Album)
    case song(Song)
}

Then, make this type conform to Decodable with a small custom initializer that defers to the respective initializers of MusicKit's own types, based on the value of the type property of the resource:

extension MySearchSuggestionItem: Decodable {
    enum CodingKeys: CodingKey {
        case type
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let type = try values.decode(String.self, forKey: .type)
        switch type {
            case "albums":
                let album = try Album(from: decoder)
                self = .album(album)
            case "songs":
                let song = try Song(from: decoder)
                self = .song(song)
            default:
                let decodingErrorContext = DecodingError.Context(
                    codingPath: decoder.codingPath, 
                    debugDescription: "Unexpected type \"\(type)\" encountered for MySearchSuggestionItem."
                )
                throw DecodingError.typeMismatch(MySearchSuggestionItem.self, decodingErrorContext)
        }
    }
}

This is the key to solving your problem.

Optionally, you can add a custom method to describe this object in a more succinct way:

extension MySearchSuggestionItem: CustomStringConvertible {
    var description: String {
        let description: String
        switch self {
            case .album(let album):
                description = "MySearchSuggestionItem.album(\(album))"
            case .song(let song):
                description = "MySearchSuggestionItem.song(\(song))"
        }
        return description
    }
}

Then you can use this type in your larger structure to decode the search suggestions response:

struct MyCatalogSearchSuggestionsResponse: Decodable {
    struct Results: Decodable {
        struct TopResultSuggestion: Decodable {
            let content: MySearchSuggestionItem
        }
        
        let suggestions: [TopResultSuggestion]
    }
    
    let results: Results
}

Then here's sample code tying this all together:

let countryCode = try await MusicDataRequest.currentCountryCode

var searchSuggestionsURLComponents = URLComponents()
searchSuggestionsURLComponents.scheme = "https"
searchSuggestionsURLComponents.host = "api.music.apple.com"
searchSuggestionsURLComponents.path = "/v1/catalog/\(countryCode)/search/suggestions"
searchSuggestionsURLComponents.queryItems = [
    URLQueryItem(name: "term", value: "discovery"), 
    URLQueryItem(name: "kinds", value: "topResults"), 
    URLQueryItem(name: "types", value: "albums,songs"), 
]
let searchSuggestionsURL = searchSuggestionsURLComponents.url!

let searchSuggestionsDataRequest = MusicDataRequest(urlRequest: URLRequest(url: searchSuggestionsURL))
let searchSuggestionsDataResponse = try await searchSuggestionsDataRequest.response()

let decoder = JSONDecoder()
let searchSuggestionsResponse = try decoder.decode(MyCatalogSearchSuggestionsResponse.self, from: searchSuggestionsDataResponse.data)

for topResultSuggestions in searchSuggestionsResponse.results.suggestions {
    print("\(topResultSuggestions.content)")
}

And here's the output produced by this code:

MySearchSuggestionItem.album(Album(id: "697194953", title: "Discovery", artistName: "Daft Punk"))
MySearchSuggestionItem.song(Song(id: "1440808397", title: "The Bad Touch", artistName: "Bloodhound Gang"))
MySearchSuggestionItem.song(Song(id: "1290141098", title: "Discovery Channel", artistName: "Lika Morgan"))
MySearchSuggestionItem.song(Song(id: "1440767972", title: "The Bad Touch", artistName: "Bloodhound Gang"))
MySearchSuggestionItem.album(Album(id: "1434140802", title: "Discovery", artistName: "Rivers & Robots"))

I hope this helps.

Best regards,

Hey @JoeKun,

Thanks for taking the time out to reply with this detailed answer, I am still new to complex JSON serialization and so this is really really helpful.

If seen your other replies on other threads and they've all been enormously helpful, so thanks again!

Hi @JoeKun, with the current solution and the response from the endpoint, it only fetches one item at a time. How would I go about creating a MusicItemCollection out of it? The Search for Catalog Resources has a very nice response as it provides an array of albums, songs, etc.

But the Get Catalog Search Suggestions has one response model per music item, be it an album, song. Etc.

Any suggestion?

Edit: Hacky workaround

switch topResult.content {
            case .album(let album): self.albums += MusicItemCollection(arrayLiteral: album)
            case .artist(let artist): self.artists += MusicItemCollection(arrayLiteral: artist)
            case .song(let song): self.songs += MusicItemCollection(arrayLiteral: song)
            case .curator(let curator): self.curators += MusicItemCollection(arrayLiteral: curator)
            default: ()
          }

Do you think I should use MusicItemCollection in this way?

Hello @snuff4,

The MusicItemCollection initializer init(arrayLiteral:) is not meant to be used explicitly as you're showing. This initializer is meant to be used by the compiler when you use the array literal syntax, as such:

    case .album(let album):
        self.albums += [album]

Please see the documentation for ExpressibleByArrayLiteral for more information.

That said, the value of splitting the top results list as you're showing is questionable. If you really want that, then why not just use MusicCatalogSearchRequest?

Thinking about the natural use-cases for suggestions, you'd probably conclude that they're most useful to show a list of terms for autocompletion, and a single list of items of different types.

I hope this helps.

Best regards,

Hello @ashinthetray and @snuff4,

I just wanted to let you know you no longer need to use MusicDataRequest on iOS 16 beta 1 to load search suggestions for the Apple Music catalog.

Instead, you can now get the same benefits with a brand new structured request in MusicKit: MusicCatalogSearchSuggestionsRequest.

I hope you'll like it!

Best regards,

How to decode the JSON response from MusicKit Search Suggestion
 
 
Q