SwiftのJSONDecoder
はdateDecodingStrategy
プロパティを提供します。これにより、DateFormatter
オブジェクトに従って着信日付文字列を解釈する方法を定義できます。
ただし、現在、両方の日付文字列(yyyy-MM-dd
)および日時文字列(yyyy-MM-dd HH:mm:ss
)、プロパティに応じて。提供されるJSONDecoder
オブジェクトは一度に1つのDateFormatter
しか処理できないため、dateFormat
でこれを処理する方法はありますか?
ハムハンドの解決策の1つは、付随するDecodable
モデルを書き換えて、プロパティとして文字列のみを受け入れ、パブリックDate
ゲッター/セッター変数を提供することですが、それは私にとっては貧弱な解決策のようです。何かご意見は?
これに対処するには、いくつかの方法があります。
DateFormatter
サブクラスを作成して、最初に日時文字列形式を試行し、失敗した場合はプレーンな日付形式を試行できます.custom
_ Date
デコード戦略を与えることができます。この戦略では、singleValueContainer()
をDecoder
に要求し、文字列をデコードし、以前に必要なフォーマッタに渡します。解析された日付を渡すinit(from:)
およびencode(to:)
を提供するDate
型のラッパーを作成できます(ただし、これは実際には_.custom
_戦略)init(from:)
を提供し、そこで異なることを試みることができます全体として、最初の2つのメソッドは最も簡単でクリーンなものになる可能性が高いです。タイプセーフを犠牲にすることなく、Codable
のデフォルトの合成実装をどこにでも保持できます。
これと同様に構成されたデコーダを試してください:
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
// possible date strings: "2016-05-01", "2016-07-04T17:37:21.119229Z", "2018-05-20T15:00:00Z"
let len = dateStr.count
var date: Date? = nil
if len == 10 {
date = dateNoTimeFormatter.date(from: dateStr)
} else if len == 20 {
date = isoDateFormatter.date(from: dateStr)
} else {
date = self.serverFullDateFormatter.date(from: dateStr)
}
guard let date_ = date else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateStr)")
}
print("DATE DECODER \(dateStr) to \(date_)")
return date_
})
return decoder
}()
この同じ問題に直面して、私は次の拡張機能を作成しました。
extension JSONDecoder.DateDecodingStrategy {
static func custom(_ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?) -> JSONDecoder.DateDecodingStrategy {
return .custom({ (decoder) -> Date in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No Coding Path Found"))
}
guard let container = try? decoder.singleValueContainer(),
let text = try? container.decode(String.self) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date text"))
}
guard let dateFormatter = try formatterForKey(codingKey) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "No date formatter for date text")
}
if let date = dateFormatter.date(from: text) {
return date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(text)")
}
})
}
}
この拡張機能を使用すると、同じJSON文字列内で複数の異なる日付形式を処理するJSONDecoderのDateDecodingStrategyを作成できます。この拡張機能には、CodingKeyを提供するクロージャーの実装を必要とする関数が含まれており、提供されたキーに正しいDateFormatterを提供するのはユーザー次第です。
次のJSONがあるとしましょう。
{
"publication_date": "2017-11-02",
"opening_date": "2017-11-03",
"date_updated": "2017-11-08 17:45:14"
}
次の構造体:
struct ResponseDate: Codable {
var publicationDate: Date
var openingDate: Date?
var dateUpdated: Date
enum CodingKeys: String, CodingKey {
case publicationDate = "publication_date"
case openingDate = "opening_date"
case dateUpdated = "date_updated"
}
}
次に、JSONをデコードするには、次のコードを使用します。
let dateFormatterWithTime: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
let dateFormatterWithoutTime: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (key) -> DateFormatter? in
switch key {
case ResponseDate.CodingKeys.publicationDate, ResponseDate.CodingKeys.openingDate:
return dateFormatterWithoutTime
default:
return dateFormatterWithTime
}
})
let results = try? decoder.decode(ResponseDate.self, from: data)
これを試して。 (スイフト4)
let formatter = DateFormatter()
var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
if let date = formatter.date(from: dateString) {
return date
}
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Cannot decode date string \(dateString)")
}
return decoder
}
単一のエンコーダーでこれを行う方法はありません。ここでの最善の策は、encode(to encoder:)
およびinit(from decoder:)
メソッドをカスタマイズし、これらの値の1つに対して独自の翻訳を提供し、組み込みの日付戦略を他の戦略に残すことです。
この目的のために、1つ以上のフォーマッターをuserInfo
オブジェクトに渡すことを検討する価値があるかもしれません。
Swift 5
実際にJSONDecoder
拡張子を使用した@BrownsooHanバージョンに基づいています
JSONDecoder + dateDecodingStrategyFormatters.Swift
_extension JSONDecoder {
/// Assign multiple DateFormatter to dateDecodingStrategy
///
/// Usage :
///
/// decoder.dateDecodingStrategyFormatters = [ DateFormatter.standard, DateFormatter.yearMonthDay ]
///
/// The decoder will now be able to decode two DateFormat, the 'standard' one and the 'yearMonthDay'
///
/// Throws a 'DecodingError.dataCorruptedError' if an unsupported date format is found while parsing the document
var dateDecodingStrategyFormatters: [DateFormatter]? {
@available(*, unavailable, message: "This variable is meant to be set only")
get { return nil }
set {
guard let formatters = newValue else { return }
self.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
for formatter in formatters {
if let date = formatter.date(from: dateString) {
return date
}
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
}
}
}
}
_
設定のみ可能な変数を追加するのはちょっとした方法ですが、func setDateDecodingStrategyFormatters(_ formatters: [DateFormatter]? )
によって_var dateDecodingStrategyFormatters
_を簡単に変換できます
使用法
あなたは既にあなたのコードでいくつかのDateFormatter
sを次のように定義していると言うことができます:
_extension DateFormatter {
static let standardT: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return dateFormatter
}()
static let standard: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter
}()
static let yearMonthDay: DateFormatter = {
var dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter
}()
}
_
dateDecodingStrategyFormatters
を設定することで、すぐにこれらをデコーダに割り当てることができます:
_// Data structure
struct Dates: Codable {
var date1: Date
var date2: Date
var date3: Date
}
// The Json to decode
let jsonData = """
{
"date1": "2019-05-30 15:18:00",
"date2": "2019-05-30T05:18:00",
"date3": "2019-04-17"
}
""".data(using: .utf8)!
// Assigning mutliple DateFormatters
let decoder = JSONDecoder()
decoder.dateDecodingStrategyFormatters = [ DateFormatter.standardT,
DateFormatter.standard,
DateFormatter.yearMonthDay ]
do {
let dates = try decoder.decode(Dates.self, from: jsonData)
print(dates)
} catch let err as DecodingError {
print(err.localizedDescription)
}
_
補足
dateDecodingStrategyFormatters
をvar
として設定するのは少しおかしいことを改めて認識しています。お勧めしません。代わりに関数を定義する必要があります。しかし、そうすることは個人的な好みです。
単一のモデルに異なる形式の複数の日付がある場合、適用するのは少し難しい.dateDecodingStrategy
各日付。
こちらをご覧ください https://Gist.github.com/romanroibu/089ec641757604bf78a390654c437cb 便利なソリューション
これは少し冗長ですが、より柔軟なアプローチです。日付を別のDateクラスでラップし、そのためのカスタムシリアル化メソッドを実装します。例えば:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
class MyCustomDate: Codable {
var date: Date
required init?(_ date: Date?) {
if let date = date {
self.date = date
} else {
return nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let string = dateFormatter.string(from: date)
try container.encode(string)
}
required public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let raw = try container.decode(String.self)
if let date = dateFormatter.date(from: raw) {
self.date = date
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot parse date")
}
}
}
だから今、あなたは.dateDecodingStrategy
および.dateEncodingStrategy
とMyCustomDate
の日付は、指定された形式で解析されます。クラスで使用します:
class User: Codable {
var dob: MyCustomDate
}
でインスタンス化
user.dob = MyCustomDate(date)