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"
      }
    ]
  }
}

Accepted Reply

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,

Replies

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 @ashinthetray,

    My pleasure! I'm glad to hear that people are making good use of MusicKit for Swift! Cheers,

Add a Comment

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?

  • I don’t fully understand the question. You should just try the sample code above. I’m pretty sure it works well. It may not use a MusicItemCollection, but there’s an array of items in there. Please try it out, and report back.

  • Yes, it does! For simplicity, I wanted to have separate arrays of songs, albums, etc., like how MusicKit has in MusicCatalogSearchResponse like albums: MusicItemCollection<Album>, songs: MusicItemCollection<Song> if that makes sense. Otherwise, there is no problem with the current implementation!

  • That’s not desirable for top results though. The point of top results is that they’re ranked across different types. So they should be exposed as a single sequence of items of different types.

Add a Comment

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,

  • That def makes sense. Thanks for the clarification! I just had a look at the Apple Music app, and they are also following the way you mentioned, one item at a time instead of a different list for each music item.

  • After looking at the response from the endpoint once again, it is very much designed to be not a list of items, rather than a few items, as it is "top" ones. I will have to rethink why I wanted them as lists.

Add a Comment

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,