私は古いJSON解析コードをSwiftのCodableに置き換えていますが、ちょっとした障害に直面しています。 DateFormatterの質問ほどコード化可能な質問ではないようです。
構造体で開始
struct JustADate: Codable {
var date: Date
}
およびJSON文字列
let json = """
{ "date": "2017-06-19T18:43:19Z" }
"""
今すぐデコードできます
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let data = json.data(using: .utf8)!
let justADate = try! decoder.decode(JustADate.self, from: data) //all good
しかし、たとえば秒の小数部を含むように日付を変更すると、次のようになります。
let json = """
{ "date": "2017-06-19T18:43:19.532Z" }
"""
今では壊れています。日付が秒の小数部で戻ってくることもあれば、そうでないこともあります。私がそれを解決するために使用した方法は、マッピングコードで、秒の小数を含むまたは含まないdateFormatの両方を試す変換関数を持っていました。しかし、Codableを使用してどのようにアプローチするかはよくわかりません。助言がありますか?
2つの異なる日付フォーマッター(小数秒ありとなし)を使用して、カスタムDateDecodingStrategyを作成できます。 APIによって返された日付の解析中にエラーが発生した場合、@ PauloMattosのコメントで示唆されているようにDecodingErrorをスローできます。
iOS 9、macOS 10.9、tvOS 9、watchOS 2、Xcode 9以降
カスタム ISO8601 DateFormatter:
extension Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
static let iso8601noFS: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
return formatter
}()
}
カスタムDateDecodingStrategy
およびError
:
extension JSONDecoder.DateDecodingStrategy {
static let customISO8601 = custom {
let container = try $0.singleValueContainer()
let string = try container.decode(String.self)
if let date = Formatter.iso8601.date(from: string) ?? Formatter.iso8601noFS.date(from: string) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
}
カスタムDateEncodingStrategy
:
extension JSONEncoder.DateEncodingStrategy {
static let customISO8601 = custom {
var container = $1.singleValueContainer()
try container.encode(Formatter.iso8601.string(from: $0))
}
}
編集/更新:
Xcode 9•Swift 4•iOS 11以降
ISO8601DateFormatter
がiOS11以降でformatOptions
.withFractionalSeconds
をサポートするようになりました:
extension Formatter {
static let iso8601: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
static let iso8601noFS = ISO8601DateFormatter()
}
習慣DateDecodingStrategy
とDateEncodingStrategy
は、上記と同じです。
// Playground testing
struct ISODates: Codable {
let dateWith9FS: Date
let dateWith3FS: Date
let dateWith2FS: Date
let dateWithoutFS: Date
}
let isoDatesJSON = """
{
"dateWith9FS": "2017-06-19T18:43:19.532123456Z",
"dateWith3FS": "2017-06-19T18:43:19.532Z",
"dateWith2FS": "2017-06-19T18:43:19.53Z",
"dateWithoutFS": "2017-06-19T18:43:19Z",
}
"""
let isoDatesData = Data(isoDatesJSON.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .customISO8601
do {
let isoDates = try decoder.decode(ISODates.self, from: isoDatesData)
print(Formatter.iso8601.string(from: isoDates.dateWith9FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601.string(from: isoDates.dateWith3FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601.string(from: isoDates.dateWith2FS)) // 2017-06-19T18:43:19.530Z
print(Formatter.iso8601.string(from: isoDates.dateWithoutFS)) // 2017-06-19T18:43:19.000Z
} catch {
print(error)
}
@Leoの答えの代わりに、古いOS(_ISO8601DateFormatter
_はiOS 10、mac OS 10.12以降でのみ使用可能)のサポートを提供する必要がある場合、解析時に両方の形式を使用するカスタムフォーマッターを書くことができます文字列:
_class MyISO8601Formatter: DateFormatter {
static let formatters: [DateFormatter] = [
iso8601Formatter(withFractional: true),
iso8601Formatter(withFractional: false)
]
static func iso8601Formatter(withFractional fractional: Bool) -> DateFormatter {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss\(fractional ? ".SSS" : "")XXXXX"
return formatter
}
override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
guard let date = (type(of: self).formatters.flatMap { $0.date(from: string) }).first else {
error?.pointee = "Invalid ISO8601 date: \(string)" as NSString
return false
}
obj?.pointee = date as NSDate
return true
}
override public func string(for obj: Any?) -> String? {
guard let date = obj as? Date else { return nil }
return type(of: self).formatters.flatMap { $0.string(from: date) }.first
}
}
_
、日付デコード戦略として使用できます:
_let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
_
実装は少しいですが、これには、エラー報告メカニズムを変更しないため、不正なデータの場合にSwiftがスローする)デコードエラーと一貫性があるという利点があります。
例えば:
_struct TestDate: Codable {
let date: Date
}
// I don't advocate the forced unwrap, this is for demo purposes only
let jsonString = "{\"date\":\"2017-06-19T18:43:19Z\"}"
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
do {
print(try decoder.decode(TestDate.self, from: jsonData))
} catch {
print("Encountered error while decoding: \(error)")
}
_
TestDate(date: 2017-06-19 18:43:19 +0000)
を出力します
小数部分の追加
_let jsonString = "{\"date\":\"2017-06-19T18:43:19.123Z\"}"
_
同じ出力になります:TestDate(date: 2017-06-19 18:43:19 +0000)
ただし、間違った文字列を使用する場合:
_let jsonString = "{\"date\":\"2017-06-19T18:43:19.123AAA\"}"
_
データが正しくない場合、デフォルトのSwift=エラーを出力します:
_Encountered error while decoding: dataCorrupted(Swift.DecodingError.Context(codingPath: [__lldb_expr_84.TestDate.(CodingKeys in _B178608BE4B4E04ECDB8BE2F689B7F4C).date], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))
_