You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何建模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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:39:08