Incorrectly Decodable response for MusicItemCollection

I'm trying to fetch the top playlists from Apple Music using the charts API and I have the following code:

func fetchPopularPlaylists() async throws -> [MusicItemCollection<Playlist>] {
    var request = URLRequest(url: URL(string: "https://api.music.apple.com/v1/catalog/US/charts?types=playlists")!)
    let userToken = try await fetchUserToken()
    request.httpMethod = "GET"
    request.addValue("Bearer \(Self.jwt)", forHTTPHeaderField: "Authorization")
    request.addValue(userToken, forHTTPHeaderField: "Music-User-Token")
    let dataRequest = MusicDataRequest(urlRequest: request)
    let response = try await dataRequest.response()
    let decoder = JSONDecoder()
    let playlistResponse = try decoder.decode(MyChartsResponse.self, from: response.data)
    return playlistResponse.results.playlists
}

And this code works well and as expected, I'm able to populate results. However, I believe that the response is getting decoded incorrectly.

My response model object:

struct MyChartsResponse: Codable {
    struct Results: Codable {
        let playlists: [MusicItemCollection<Playlist>]
    }
    let results: Results
}

The charts api however has the response listed in the form of:

{
     "results": {
        "playlists": [
            // playlist entities here 
        ]
    }
}

This API response makes it seem like the proper Swift decoding structure is either

struct MyChartsResponse: Codable {

    struct Results: Codable {
        let playlists: MusicItemCollection<Playlist>
    }
    let results: Results
}

or

struct MyChartsResponse: Codable {

    struct Results: Codable {
        let playlists: [Playlist]
    }
    let results: Results
}

but instead the playlist property has to be Array<MusicItemCollection<Playlist>> in order to decode correctly with no custom decoding. The playlists property is incorrectly getting wrapped in an extraneous collection on decoding which makes extracting it difficult.

Replies

Hello @startupthekid,

Thank you very much for your question about how to decode charts from Apple Music API.

When using MusicKit for Swift, MusicItemCollection can be used to decode a resource collection from Apple Music API.

So let's take a closer look at the response from the Get Catalog Charts endpoint:

"results" : {
    "playlists" : [
        {
            "data" : [
                {
                    "id" : "pl.b0e04e25887741ea845e1d5c88397fd4",
                    "type" : "playlists",
                    "href" : "\/v1\/catalog\/us\/playlists\/pl.b0e04e25887741ea845e1d5c88397fd4",
                    [...]
                },
                [...]
            ]
        }
    ]
}

As it turns out, the part of that response that corresponds to resource collection of playlists is this:

        {
            "data" : [
                {
                    "id" : "pl.b0e04e25887741ea845e1d5c88397fd4",
                    "type" : "playlists",
                    "href" : "\/v1\/catalog\/us\/playlists\/pl.b0e04e25887741ea845e1d5c88397fd4",
                    [...]
                },
                [...]
            ]
        }

Hence the server raw response is returning as results.playlists an array of a resource collection of playlists:

"results" : {
    "playlists" : [
        [... resource collection of playlists ...]
    ]
}

That's why your initial Codable-conforming structure is actually correct:

struct MyChartsResponse: Codable {
    struct Results: Codable {
        let playlists: [MusicItemCollection<Playlist>]
    }
    let results: Results
}

So based on this analysis, we can see that MusicKit for Swift is behaving correctly.

While I personally agree with your sentiment that we shouldn't have to have an array of a resource collection, the reality is that that's what Apple Music API actually returns from the Get Catalog Charts endpoint.

Furthermore, I need to clarify something important about MusicDataRequest. Unlike your past experiences trying to load data from Apple Music API before the release of MusicKit for Swift, when you use requests from this new framework, you do not need to worry about decorating your URLRequest with either a developer token or a Music-User-Token. Hence, you can safely remove the following code:

    let userToken = try await fetchUserToken()
    request.httpMethod = "GET"
    request.addValue("Bearer \(Self.jwt)", forHTTPHeaderField: "Authorization")
    request.addValue(userToken, forHTTPHeaderField: "Music-User-Token")

The following should work just as well:

func fetchPopularPlaylists() async throws -> MusicItemCollection<Playlist> {
    let url = URL(string: "https://api.music.apple.com/v1/catalog/us/charts?types=playlists")!
    let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
    let dataResponse = try await dataRequest.response()
    let playlistsResponse = try decoder.decode(MyChartsResponse.self, from: dataResponse.data)
    return playlistsResponse.results.playlists.first ?? []
}

Lastly, I would recommend you file a new ticket on Feedback Assistant asking for a new public API to be created in MusicKit for Swift to make it easier to get catalog charts. Getting your input as an actual ticket will make it easier for us to prioritize this task in our roadmap.

I hope this helps.

Best regards,

It doesn't matter if it matches the API or not, it is an incorrect pluralization of "collections". You can match the SDK api to the Apple Music API all you want, that doesn't change the fact that the underlying api design is incorrect and therefore, not working as expected.

Hello @startupthekid,

Thanks for the additional feedback.

I would encourage you to file a ticket on Feedback Assistant to explain how you think we could improve the response from the Get Catalog Charts endpoint.

Once you file that ticket, I can send it to my colleagues working on the team responsible for Apple Music API.

Best regards,

Hello @JoeKun,

I am also having trouble decoding the chart data. I was able to get a MusicItemCollection with the same implementation as you described above.

However, the following error occurs when trying to get nextBatch.

let data = try await albums.nextBatch()
keyNotFound(CodingKeys(stringValue: "data", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"data\", intValue: nil) (\"data\").", underlyingError: nil))

How do I get nextBatch about a chart?

I thought it might have something to do with the fact that the resource collection is an array, so I added it here.

Hello @birnimal,

Thank you for finding this issue.

Indeed, MusicItemCollection's nextBatch(…) doesn't currently know how to handle next batches from the catalog charts endpoint.

We already have a Feedback Assistant ticket about exposing catalog charts as another structured request in MusicKit framework: FB9583849.

I believe that's the right approach to address this issue.

I'm making note of your interest for this kind of functionality in MusicKit.

Best regards,

  • Thank you @JoeKun. I too think it would be best to change the structure. I hope the specs will be updated.

Add a Comment

Hello @startupthekid and @birnimal,

We have introduced a new structured request in iOS 16 beta 1 for fetching Apple Music catalog charts in Swift: MusicCatalogChartsRequest.

It's now easier than ever to load catalog charts with MusicKit, and, of course, you can check if a given chart’s items collection has a next batch, and load it using nextBatch(…).

I hope this helps.

Best regards,