JSONDecoder 解析時間的 DateDecodingStrategy
當我們利用 JSONDecoder 串接 API 回傳的 JSON 資料時,通常我們會將時間欄位的資料對應到 Date 型別,方便之後App 使用一些時間運算的 function。不過 API 回傳的時間有許多格式,因此我們須告訴 JSONDecoder 時間的格式,它才能將時間解析後儲存成 Date 型別。
JSONDecoder 解析時間的 DateDecodingStrategy
我們可透過 JSONDecoder 的 dateDecodingStrategy 設定 JSON 的時間格式。
dateDecodingStrategy 的型別是 DateDecodingStrategy,我們可設定的格式有以下幾種,接下來讓我們以實際的例子介紹幾種常見的時間格式。
enum DateDecodingStrategy {
case deferredToDate
case secondsSince1970
case millisecondsSince1970
case iso8601
case formatted(DateFormatter)
case custom((Decoder) throws -> Date)
}
iso8601
範例: iTunes API。
https://itunes.apple.com/search?term=beatles&media=music
以下 releaseDate 的格式稱為 iso8601,內容為 1970–03–06T12:00:00Z,特點是日期跟時間中間有個 T,結尾有個 Z。
struct SearchResponse: Decodable {
let results: [StoreItem]
}
struct StoreItem: Decodable {
let trackName: String
let releaseDate: Date
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
將 dateDecodingStrategy 設為 iso8601。
secondsSince1970
範例: IG API。
https://www.instagram.com/lepetitprinceofficiel/?__a=1&__d=dis
以下 taken_at_timestamp 的格式稱為 secondsSince1970,內容為 1586871207,它代表距離 1970.1.1 的秒數。
當後台回傳的時間是一串數字時,有很大的機率是距離 1970.1.1 的秒數。我們也可以先在 Xcode playground 生成 Date,在參數 timeIntervalSince1970 傳入後台回傳的數字測試生成的 Date 是否合理。
struct IGResult: Decodable {
let graphql: Graphql
}
struct Graphql: Decodable {
let user: User
}
struct User: Decodable {
let edgeOwnerToTimelineMedia: TimelineMedia
}
struct TimelineMedia: Decodable {
let edges: [Edge]
}
struct Edge: Decodable {
let node: Node
}
struct Node: Decodable {
let takenAtTimestamp: Date
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
將 dateDecodingStrategy 設為 secondsSince1970。
millisecondsSince1970
範例: covid19uk API。
https://api.covid19uk.live/historyfigures
此 API 目前已經失效,不過從下圖可看出之前抓到的 JSON 資料。以下 date 的格式稱為 millisecondsSince1970,內容為 1580428200000,它代表距離 1970.1.1 的毫秒數。一秒等於 1000 毫秒,因此 1580428200000 毫秒等於 1580428200 秒。
前面提過後台回傳的時間是一串數字時,有很大的機率是距離 1970.1.1 的秒數,不過它也可能是距離 1970.1.1 的毫秒數。因此如果以 secondsSince1970 得到數字很大的不正常時間時,記得再用 millisecondsSince1970 試試。
struct CovidData: Decodable {
let data: [Record]
}
struct Record: Decodable {
let date: Date
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
將 dateDecodingStrategy 設為 millisecondsSince1970。
formatted
範例: 口罩 API。
https://quality.data.gov.tw/dq_download_json.php?nid=116285&md5_url=2150b333756e64325bdbc4a5fd45fad1
以下來源資料時間的格式是後台自訂的格式,內容為 2020/04/15 14:54:00。JSONDecoder 沒那麼聰明,無法解析後台自訂的格式,因此我們必須透過 DateFormatter 幫助 JSONDecoder 看懂它的格式。
struct Mask: Decodable {
let name: String
let time: Date
enum CodingKeys: String, CodingKey {
case name = "醫事機構名稱"
case time = "來源資料時間"
}
}
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
將 dateDecodingStrategy 設為 formatted,傳入自訂的 DateFormatter。
其它例子:
custom
範例: Dcard API。
https://dcard.tw/_api/posts?popular=true
目前無法直接連到 Dcard API 抓資料。若想測試可從 Safari 連到 API 網址,抓取 JSON 後將 JSON 貼到程式裡。以下 createdAt 的格式是帶有小數點秒數的 iso8601,內容為 2020–04–15T04:24:20.683Z
,但剛剛介紹的 iso8601 只能解析沒有小數點的 iso8601。
此時我們有兩種解法。我們可用剛剛介紹的 formatted,在 dateFormat 設定時間格式。
struct Dcard: Decodable {
let title: String
let createdAt: Date
}
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
我們也可以用另一種方法,使用 custom 搭配 iOS SDK 的 ISO8601DateFormatter 解析帶有小數點的 iso8601。
let decoder = JSONDecoder()
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
if let date = dateFormatter.date(from: dateString) {
return date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "")
}
})
說明:
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
在 ISO8601DateFormatter 的 formatOptions 裡傳入 withFractionalSeconds,代表 iso8601 有小數點的秒數。
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
if let date = dateFormatter.date(from: dateString) {
return date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "")
}
})
JSONDecoder 解析 JSON 時,當它遇到我們宣告為 Date 型別的欄位,比方例子裡的 createdAt,它會透過我們在 custom 傳入的 closure 解析。decoder.singleValueContainer()
將取得 createdAt 的內容,由於它是字串,所以我們先用 container.decode(String.self)
得到它的字串,然後再透過 dateFormatter.date(from: dateString)
將字串變成 Date。若是失敗則丟出 DecodingError。