続・イケてない JSON を Swift の Decodable で扱いやすいモデルにデコードする
イケてない JSON を Swift の Decodable で扱いやすいモデルにデコードする の続き。
今度も last.fm の user.getRecentTracks を Decode していく。
尚、文字を打つのが面倒になったためこの記事はあらゆる説明を省略している。
JSON
これを decode していく。
{ "recenttracks": { "track": [ { "artist": { "#text": "TOKIO", "mbid": "d210bd3e-68db-4987-a714-0214449e361d" }, "name": "宙船", "streamable": "0", "mbid": "", "album": { "#text": "Harvest", "mbid": "3824d059-6f1a-4163-ae1a-65d5f87bee38" }, "url": "https://www.last.fm/music/TOKIO/_/%E5%AE%99%E8%88%B9", "image": [ { "#text": "https://lastfm-img2.akamaized.net/i/u/34s/1a20e578c4ea44c0a8cde03d6c6cc014.png", "size": "small" }, { "#text": "https://lastfm-img2.akamaized.net/i/u/64s/1a20e578c4ea44c0a8cde03d6c6cc014.png", "size": "medium" } ], "@attr": { "nowplaying": "true" } }, ............... ], "@attr": { "user": "star__hoshi", "page": "1", "perPage": "50", "totalPages": "807", "total": "40316" } } }
List 系 API の共通要素
last.fm の List 系 API は、データ本体の配列と attr という API メタデータの 2 つから構成されていて他の API でも同じなので ListResponse という protocol を用意した。
この List
に RecentTrack や TopTrack を入れると Generics でいい感じになる。
public protocol ListResponse { associatedtype List var list: [List] { get } var attr: Attr { get } } public struct Attr: Decodable { public let user: String public let page: Int public let perPage: Int public let totalPages: Int public let total: Int }
protocol を実装するとこんな感じになる。
public struct RecentTracksResponse: ListResponse, Decodable { public typealias List = RecentTrack public let list: [List] public let attr: Attr private enum CodingKeys: String, CodingKey { case list case attr } private enum RecentTracksKeys: String, CodingKey { case recenttracks } private enum TrackAttrKeys: String, CodingKey { case track case attr = "@attr" } public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: RecentTracksKeys.self) let recentTracks = try values.nestedContainer(keyedBy: TrackAttrKeys.self, forKey: .recenttracks) self.attr = try recentTracks.decode(Attr.self, forKey: .attr) self.list = try recentTracks.decode([List].self, forKey: .track) } }
配列の中身を decode
nowplaying がネストしててもしょうもないので、 @attr
で decode してそこからさらに nowplaying
で decode するようにした。
last.fm の API は本当にひどいので、 nowplaying
が false 場合はそもそも @attr
が空っぽになってる。なので if decoder.contains(.nowplaying)
してから decode している。
Image とか String -> Int の変換とかは こっち で先にやっていたので使い回せて便利。
public struct RecentTrack: Decodable { public let name: String public let image: Image public let loved: Bool? public let streamable: Bool public let mbid: String public let url: URL public let date: Date? public let nowplaying: Bool public let album: Album public let artist: RecentArtist private enum CodingKeys: String, CodingKey { case name case image case loved case streamable case mbid case url case date case album case artist case nowplaying = "@attr" } private enum NowplayingKeys: String, CodingKey { case nowplaying } public init(from decoder: Decoder) throws { let decoder = try decoder.container(keyedBy: CodingKeys.self) name = try decoder.decode(String.self, forKey: .name) image = try decoder.decode(ImageDecodableMap.self, forKey: .image).decoded let loved = try decoder.decodeIfPresent(StringCodableMap<Int>.self, forKey: .loved)?.decoded self.loved = loved == nil ? nil : loved == 1 streamable = try decoder.decode(StringCodableMap<Int>.self, forKey: .streamable).decoded == 1 mbid = try decoder.decode(String.self, forKey: .mbid) url = try decoder.decode(URL.self, forKey: .url) date = try decoder.decodeIfPresent(DateDecodableMap.self, forKey: .date)?.decoded album = try decoder.decode(Album.self, forKey: .album) artist = try decoder.decode(RecentArtist.self, forKey: .artist) if decoder.contains(.nowplaying) { let nowplayingDecoder = try decoder.nestedContainer(keyedBy: NowplayingKeys.self, forKey: .nowplaying) nowplaying = try nowplayingDecoder.decodeIfPresent(StringCodableMap<Bool>.self, forKey: .nowplaying)?.decoded ?? false } else { nowplaying = false } } }
最終的には LastfmClient/RecentTrack.swift な感じになった。
本当はもっとややこしくて、 extended=0
で API 投げると loved や data が帰ってこなかったりするのだが、それは decodeIfPresent
でなんとかなる。
おわり
Codable 良いのだが、汚い API は Decode するだけで無限に時間を消費しててつらい。