JSONDecoder 解析時間的 DateDecodingStrategy

當我們利用 JSONDecoder 串接 API 回傳的 JSON 資料時,通常我們會將時間欄位的資料對應到 Date 型別,方便之後App 使用一些時間運算的 function。不過 API 回傳的時間有許多格式,因此我們須告訴 JSONDecoder 時間的格式,它才能將時間解析後儲存成 Date 型別。

以上三種 API 分別回傳不同的時間格式

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。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com