0

I'm trying to use classes, structs, and Decodableto make a response hierarchy for easy and automatic decoding of Apple Music API response. The Apple Music API response objects are kind of confusing though, and I'm not sure how to make a structure that allows extensibility in the future when I expand the types that subclass other types. Here's what I have so far:

A ResponseRoot object in the Apple Music API looks like this for me:

class AMResponseRoot : Decodable {
    
    let data: [AMResourceResponse]?
    let next: String?
//    let results: AppleMusicResults?
//    let errors: [AppleMusicError]?
    
    private enum AppleMusicResponseRootCodingKeys : String, CodingKey {
        case data = "data"
        case next = "next"
        case results = "results"
        case errors = "errors"
    }
    
    required init(from decoder: Decoder) throws {
        let rootContainer = try decoder.container(keyedBy: AppleMusicResponseRootCodingKeys.self)
        
        self.data = try rootContainer.decodeIfPresent([AMResourceResponse].self, forKey: .data)
        self.next = try rootContainer.decodeIfPresent(String.self, forKey: .next)
//        self.results = try rootContainer.decodeIfPresent(AppleMusicResults.self, forKey: .results)
//        self.errors = try rootContainer.decodeIfPresent([Error].self, forKey: .errors)
    }
    
}

I've commented out the results and errors decoding for now, since I'm not sure how decoding works for those/I don't know what the AppleMusicResponse object will look like.

This ResponseRoot is pretty straightforward, I just have a dynamic class/protocol type for the resource in the data object array. Now, defining what an AMResourceResponse is has been difficult, because I want to be able to subclass it or make a class adopt it as a protocol, such as when I have different types of response objects I can expect, like an AMSongResponse or AMAlbumResponse or AMPlaylistResponse, etc.

How can I make it so that each AMResourceResponse is guaranteed to have the attributes: Attributes, id: String, and type: String fields? Those are the fields I can find that are common between all types of response objects in the data field in the ResponseRoot object.

Attributes should also be defined by the subclass itself, too, because different response objects have different structures for what attributes they return. But all of them will have some sort of Attributes in the response, so I want to codify that in the response objects well. For example, a Song.Attributes object would have those specific fields, but an Album.Attributes would have different ones.

On top of this, there's a Resource object in the Apple Music API that might be useful for abstracting what a "Resource" means, and maybe it could inform my definition of AMResourceResponse somehow, idk.

Here's everything I have so far, if it helps. It's really all over the place though.

protocol AMResourceResponse {
    
    struct Attributes {
        
    }
    
    var attributes: Attributes { get }
    var id: String { get }
    var type: String { get }

}

class AMSongResponse : AMResourceResponse {
 
    struct Attributes : Decodable {
        
        let albumName: String
        let artistName: String
        let artwork: Artwork
        let name: String
        let playParams: PlayParams
        let trackNumber: Int
        
        private enum AttributesCodingKeys : String, CodingKey {
            case albumName = "albumName"
            case artistName = "artistName"
            case artwork = "artwork"
            case name = "name"
            case playParams = "playParams"
            case trackNumber = "trackNumber"
        }
        
        init(from decoder: Decoder) throws {
            let attributesContainer = try decoder.container(keyedBy: AttributesCodingKeys.self)
            
            self.albumName = try attributesContainer.decode(String.self, forKey: .albumName)
            self.artistName = try attributesContainer.decode(String.self, forKey: .artistName)
            self.artwork = try attributesContainer.decode(Artwork.self, forKey: .artwork)
            self.name = try attributesContainer.decode(String.self, forKey: .name)
            self.playParams = try attributesContainer.decode(PlayParams.self, forKey: .playParams)
            self.trackNumber = try attributesContainer.decode(Int.self, forKey: .trackNumber)
        }
        
    }
    
}

struct PlayParams : Decodable {
    
    let id: String
    let isLibrary: Bool
    let kind: String
    
    private enum PlayParamsCodingKeys : String, CodingKey {
        case id = "id"
        case isLibrary = "isLibrary"
        case kind = "kind"
    }
    
    init(from decoder: Decoder) throws {
        let playParamsContainer = try decoder.container(keyedBy: PlayParamsCodingKeys.self)
        
        self.id = try playParamsContainer.decode(String.self, forKey: .id)
        self.isLibrary = try playParamsContainer.decode(Bool.self, forKey: .isLibrary)
        self.kind = try playParamsContainer.decode(String.self, forKey: .kind)
    }
    
}
    
struct Artwork : Decodable {
    
    let height: Int
    let width: Int
    let url: String
    
    private enum ArtworkCodingKeys : String, CodingKey {
        case height = "height"
        case width = "width"
        case url = "url"
    }
    
    init(from decoder: Decoder) throws {
        let artworkContainer = try decoder.container(keyedBy: ArtworkCodingKeys.self)
        
        self.height = try artworkContainer.decode(Int.self, forKey: .height)
        self.width = try artworkContainer.decode(Int.self, forKey: .width)
        self.url = try artworkContainer.decode(String.self, forKey: .url)
    }
    
}

struct Album : Decodable {
    
    struct Attributes : Decodable {
        
        let artistName: String
        let artwork: Artwork
        let contentRating: String?
        let name: String
        let playParams: PlayParams?
        let trackCount: Int
        
    }
    
    let attributes: Attributes
    
}

struct Artist : Decodable {
    
    
    
}

struct Relationships : Decodable {
    
    struct LibraryAlbumRelationship : Decodable {
        
        let albums: [Album]
        
    }
    
    struct LibraryArtistRelationship : Decodable {
        
        let artists: [Artist]
        
    }
    
    let albumRelationships: LibraryAlbumRelationship
    let artistRelationships: LibraryArtistRelationship
    
    private enum RelationshipsCodingKeys : String, CodingKey {
        case data = "data"
    }
    
    init(from decoder: Decoder) throws {
        let attributesContainer = try decoder.container(keyedBy: RelationshipsCodingKeys.self)
        
        self.stuff = try attributesContainer.decode(String.self, forKey: .data)
    }
    
}

I'm just really scatterbrained from being deep in this and could use some direction as to how to construct this hierarchy. Thanks :)

Tyler Cheek
  • 327
  • 2
  • 14
  • You don't need to manually do decoding or specify the coding keys. You can just declare that a type conforms to `Decodable` and if all its properties conform, then the compiler would just synthesize the conformance for you. For example, for `Artwork`, you don't need to implement `init(from decoder: Decoder)` or declare `ArtworkCodingKeys` – New Dev Jun 24 '20 at 18:37
  • @NewDev, good to know! That helps shorten some of the code, but any ideas on how to structure the entire thing? – Tyler Cheek Jun 25 '20 at 03:05
  • I'm not familiar with Apple Music API and their JSON object structures. so it's not clear to me what problems you're seeing. Maybe simplify your question to a conceptual made-up example that illustrates the issue – New Dev Jun 25 '20 at 05:10

0 Answers0