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