如何建模Resource对象以解码Apple Music API返回的异构attributes JSON
解决Apple Music API Resource对象建模与JSON解码问题
我来给你捋捋这个问题的解决思路哈——要处理这种同一结构下属性随类型动态变化的JSON解码,咱们可以用Swift的枚举关联值+自定义Codable解码逻辑来搞定,具体步骤如下:
1. 先定义各资源类型的专属Attributes模型
首先得把Song、Playlist、Album各自的属性结构单独拆出来,因为它们的attributes字段差异很大:
// 歌曲属性模型 struct SongAttributes: Codable { let title: String let artistName: String let durationInMillis: Int // 这里可以根据Apple Music API的返回,补充更多歌曲专属字段 } // 歌单属性模型 struct PlaylistAttributes: Codable { let name: String let description: String? let trackCount: Int // 补充歌单专属字段 } // 专辑属性模型 struct AlbumAttributes: Codable { let name: String let artistName: String let releaseDate: String // 补充专辑专属字段 }
2. 用枚举封装多类型的Attributes
接下来定义一个枚举,把三种Attributes类型用关联值包裹起来,这样就能在Resource里统一存储不同类型的属性了:
enum ResourceAttributes { case song(SongAttributes) case playlist(PlaylistAttributes) case album(AlbumAttributes) }
3. 定义Resource结构体并实现自定义解码逻辑
核心就在这里:我们要让Resource实现Codable,并在解码时根据type字段的值,动态解析对应的Attributes类型:
struct Resource: Codable { let id: String? let type: String? let href: String? let attributes: ResourceAttributes? // 定义JSON解码的键名 enum CodingKeys: String, CodingKey { case id, type, href, attributes } // 自定义初始化(解码逻辑) init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // 先解码基础字段 id = try container.decodeIfPresent(String.self, forKey: .id) type = try container.decodeIfPresent(String.self, forKey: .type) href = try container.decodeIfPresent(String.self, forKey: .href) // 根据type字段,解码对应的Attributes guard let resourceType = type else { attributes = nil return } switch resourceType { // 注意:这里的字符串要和Apple Music API返回的type值完全匹配,比如API返回"songs"就写"songs",返回"song"就写"song" case "songs": let songAttrs = try container.decode(SongAttributes.self, forKey: .attributes) attributes = .song(songAttrs) case "playlists": let playlistAttrs = try container.decode(PlaylistAttributes.self, forKey: .attributes) attributes = .playlist(playlistAttrs) case "albums": let albumAttrs = try container.decode(AlbumAttributes.self, forKey: .attributes) attributes = .album(albumAttrs) default: // 遇到未知类型时,可以选择抛出错误,或者设为nil attributes = nil // throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "未知资源类型:\(resourceType)") } } }
4. 转换成具体的业务模型(可选但实用)
如果需要把Resource转换成更贴合业务的Song、Playlist、Album对象,可以额外定义这些模型,并给Resource加扩展做转换:
// 业务层的歌曲模型 struct Song { let id: String? let href: String? let attributes: SongAttributes } // 业务层的歌单模型 struct Playlist { let id: String? let href: String? let attributes: PlaylistAttributes } // 业务层的专辑模型 struct Album { let id: String? let href: String? let attributes: AlbumAttributes } // 给Resource加转换扩展 extension Resource { func toSong() -> Song? { guard case .song(let attrs) = attributes else { return nil } return Song(id: id, href: href, attributes: attrs) } func toPlaylist() -> Playlist? { guard case .playlist(let attrs) = attributes else { return nil } return Playlist(id: id, href: href, attributes: attrs) } func toAlbum() -> Album? { guard case .album(let attrs) = attributes else { return nil } return Album(id: id, href: href, attributes: attrs) } }
5. 实际解码示例
最后就可以用JSONDecoder正常解码API返回的数据了:
let decoder = JSONDecoder() do { // jsonData是API返回的Data类型数据 let resources = try decoder.decode([Resource].self, from: jsonData) // 把Resource数组转换成对应的业务模型数组 let songs = resources.compactMap { $0.toSong() } let playlists = resources.compactMap { $0.toPlaylist() } let albums = resources.compactMap { $0.toAlbum() } // 接下来就可以处理这些业务模型啦 } catch { print("解码出错:\(error.localizedDescription)") }
注意事项
- 一定要确保
switch里的type字符串和Apple Music API返回的完全一致,比如API返回的是单数"song"还是复数"songs",得对应上; - 遇到未知类型时,根据你的业务需求选择忽略或者抛出错误;
- 如果后续要支持更多资源类型,只需要新增对应的Attributes模型,在
ResourceAttributes枚举里加case,再在init(from:)的switch里加分支即可,扩展性很强。
内容的提问来源于stack exchange,提问作者Travis S.
相关产品推荐
相关产品推荐

