JSONSerializationでシコシコJSONをパースしていたのも今は昔。
Swift4にCodable
が現れて以降、JSONのパースは非常に手軽なものになっています。
また強力な型システムの恩恵を受けながらデータを扱えることは、潜在的なエラーの発生も抑えることができます。
ただ型が強力であるが故、逆に扱いづらくなるケースも存在します。
たとえばこんな感じです。
{
"data": [
{
"id": 1,
"embed": {
"type": "video",
"url": "./xxx.mp4",
"duration": 120
}
},
{
"id": 2,
"embed": {
"type": "book",
"url": "./xxx.pdf",
"page": 300
}
}
]
}
embed
の下にぶら下がるデータがvideo
の場合とbook
の場合があり、それぞれ付随するプロパティーが違っています。
この例で言えばduration
とpage
をオプショナルにするなどでも良いかもしれないですが、book
にsummary
やcoverImage
を追加するなど、タイプやプロパティーが増えていくと1つの型で扱うのは厳しくなります。
JSONSerialization
でセコセコ頑張るのも1つの手ですが、こういったフォーマットのJSONももちろんCodable
で扱うことができます。
上記JSONをパースするコードはこのようになります。
import Foundation
let json = """
{
"data": [
{
"id": 1,
"embed": {
"type": "video",
"url": "./xxx.mp4",
"duration": 120
}
},
{
"id": 2,
"embed": {
"type": "book",
"url": "./xxx.pdf",
"page": 300
}
}
]
}
"""
/// ルート
struct Root: Codable {
let data: [Element]
/// 要素
struct Element: Codable {
let id: Int
let embed: Embed
}
}
/// 添付データ
enum Embed: Codable {
case video(Video)
case book(Book)
/// ビデオ
struct Video: Codable {
let type: `Type`
let url: URL
let duration: Int
}
/// 本
struct Book: Codable {
let type: `Type`
let url: URL
let page: Int
}
/// タイプ
enum `Type`: String, Codable {
case video = "video"
case book = "book"
}
/// コーディングキー
private enum CodingKeys: CodingKey {
case type
}
/// デコード
/// - Parameter decoder: デコーダー
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let type = try values.decodeIfPresent(`Type`.self, forKey: .type)
switch type {
case .video:
self = .video(try Video(from: decoder))
case .book:
self = .book(try Book(from: decoder))
case .none:
let error = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unknown type")
throw DecodingError.typeMismatch(Embed.self, error)
}
}
/// エンコード
/// - Parameter encoder: エンコーダー
public func encode(to encoder: Encoder) throws {
switch self {
case .video(let video):
try video.encode(to: encoder)
case .book(let book):
try book.encode(to: encoder)
}
}
}
// デコード
let decoded = try JSONDecoder().decode(Root.self, from: json.data(using: .utf8)!)
decoded.data.forEach {
switch $0.embed {
case .video(let video):
print(video.duration) // 120
case .book(let book):
print(book.page) // 300
}
}
enum
で型の違いを吸収し、連想値で実際のデータを渡す、という方法です。
肝はデコーダーを実装している部分です。
/// デコード
/// - Parameter decoder: デコーダー
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let type = try values.decodeIfPresent(`Type`.self, forKey: .type)
switch type {
case .video:
self = .video(try Video(from: decoder))
case .book:
self = .book(try Book(from: decoder))
case .none:
let error = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unknown type")
throw DecodingError.typeMismatch(Embed.self, error)
}
}
init(from decoder: Decoder)
はDecodable
で指定されているメソッドです。
String
やInt
などはあらかじめDecodable
に適合しており、それらの型を使うだけの場合はデコーダーを実装する必要はありません。
ただし今回は、値の内容によって型を振り分ける必要があるため、デコーダを実装しています。
let values = try decoder.container(keyedBy: CodingKeys.self)
let type = try values.decodeIfPresent(`Type`.self, forKey: .type)
こちらの部分でとりあえず、type
だけパースします。
decodeIfPresent
はデコードが成功したのみだけ値を返すメソッドで、将来的にmusic
やimage
などのタイプが増えた場合に備えています。
その後タイプによってEmbed
の値を決定します。
case .video:
self = .video(try Video(from: decoder))
Video
はDecodable
にすでに適合しているため、デコーダーを渡すことで初期化できます。
場合によってはタイプがない、なんてこともあるかもしれません。
{
"data": [
{
"id": 1,
"embed": {
"url": "./xxx.mp4",
"duration": 120
}
},
{
"id": 2,
"embed": {
"url": "./xxx.pdf",
"page": 300
}
}
]
}
こういった場合も対応できます。
/// デコード
/// - Parameter decoder: デコーダー
init(from decoder: Decoder) throws {
if let video = try? Video(from: decoder) {
self = .video(video)
} else if let book = try? Book(from: decoder) {
self = .book(book)
} else {
let error = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unknown type")
throw DecodingError.typeMismatch(Embed.self, error)
}
}
型に対して随時初期化を試みることでパースするという方法です。
ただ、これに関してはJSONのフォーマットを変えた方が良いかもしれません・・・。
型の違いをenum
で吸収するというテクニックは、Appleが提供しているMusicKitから知りました。
具体的にはTrack
という部分です。
こちらは音楽ファイルとミュージックビデオを収める型になります。
ミュージックアプリのプレイリスト内はどちらも混在できるようになっており、配列としてトラック部分が渡されるためにこういった型になっています。
実際のAPIは以下のようになっており、そこをパースするためにこのテクニックが使われています。
data (Required) The ordered songs and music videos in the tracklist of the playlist.
[*] Possible types: MusicVideos, SongsPlaylists.Relationships.PlaylistsTracksRelationship | Apple Developer Documentation
Codable
はかなり柔軟にできており、どんなJSONであれ、パースできなかった経験は今のところありません。
Swiftの強みを活かす意味でも、Codable
の理解は深めていきたいと思います。