web-dev-qa-db-ja.com

JSON文字列に複数の日付形式を持つSwiftのJSONDecoder?

SwiftのJSONDecoderdateDecodingStrategyプロパティを提供します。これにより、DateFormatterオブジェクトに従って着信日付文字列を解釈する方法を定義できます。

ただし、現在、両方の日付文字列(yyyy-MM-dd)および日時文字列(yyyy-MM-dd HH:mm:ss)、プロパティに応じて。提供されるJSONDecoderオブジェクトは一度に1つのDateFormatterしか処理できないため、dateFormatでこれを処理する方法はありますか?

ハムハンドの解決策の1つは、付随するDecodableモデルを書き換えて、プロパティとして文字列のみを受け入れ、パブリックDateゲッター/セッター変数を提供することですが、それは私にとっては貧弱な解決策のようです。何かご意見は?

21
RamwiseMatt

これに対処するには、いくつかの方法があります。

  • DateFormatterサブクラスを作成して、最初に日時文字列形式を試行し、失敗した場合はプレーンな日付形式を試行できます
  • _.custom_ Dateデコード戦略を与えることができます。この戦略では、singleValueContainer()Decoderに要求し、文字列をデコードし、以前に必要なフォーマッタに渡します。解析された日付を渡す
  • これを行うカスタムinit(from:)およびencode(to:)を提供するDate型のラッパーを作成できます(ただし、これは実際には_.custom_戦略)
  • あなたが提案するように、プレーン文字列を使用できます
  • これらの日付を使用するすべてのタイプでカスタムinit(from:)を提供し、そこで異なることを試みることができます

全体として、最初の2つのメソッドは最も簡単でクリーンなものになる可能性が高いです。タイプセーフを犠牲にすることなく、Codableのデフォルトの合成実装をどこにでも保持できます。

27
Itai Ferber

これと同様に構成されたデコーダを試してください:

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
}()
32
Leszek Zarna

この同じ問題に直面して、私は次の拡張機能を作成しました。

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)
14
S.Moore

これを試して。 (スイフト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
}
9
Brownsoo Han

単一のエンコーダーでこれを行う方法はありません。ここでの最善の策は、encode(to encoder:)およびinit(from decoder:)メソッドをカスタマイズし、これらの値の1つに対して独自の翻訳を提供し、組み込みの日付戦略を他の戦略に残すことです。

この目的のために、1つ以上のフォーマッターをuserInfoオブジェクトに渡すことを検討する価値があるかもしれません。

2
Ben Scheirman

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_を簡単に変換できます

使用法

あなたは既にあなたのコードでいくつかのDateFormattersを次のように定義していると言うことができます:

_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)
}
_

補足

dateDecodingStrategyFormattersvarとして設定するのは少しおかしいことを改めて認識しています。お勧めしません。代わりに関数を定義する必要があります。しかし、そうすることは個人的な好みです。

2
Olympiloutre

単一のモデルに異なる形式の複数の日付がある場合、適用するのは少し難しい.dateDecodingStrategy各日付。

こちらをご覧ください https://Gist.github.com/romanroibu/089ec641757604bf78a390654c437cb 便利なソリューション

0
Johnykutty

これは少し冗長ですが、より柔軟なアプローチです。日付を別の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および.dateEncodingStrategyMyCustomDateの日付は、指定された形式で解析されます。クラスで使用します:

class User: Codable {
    var dob: MyCustomDate
}

でインスタンス化

user.dob = MyCustomDate(date)
0
comm1x