2017-06-21 100 views
8

Swift的JSONDecoder提供dateDecodingStrategy屬性,它允許我們根據DateFormatter對象定義如何解釋傳入日期字符串。Swift的JSONDecoder在JSON字符串中有多種日期格式?

但是,我目前正在使用一個API,該API返回日期字符串(yyyy-MM-dd)和日期時間字符串(yyyy-MM-dd HH:mm:ss),具體取決於屬性。有沒有辦法讓JSONDecoder處理這個問題,因爲提供的DateFormatter對象一次只能處理一個單獨的dateFormat

一個笨手笨腳的解決方案是重寫隨附的Decodable模型,只接受字符串作爲它們的屬性,並提供公開的getter/setter變量,但這對我來說似乎是一個糟糕的解決方案。有什麼想法嗎?

回答

15

有一些方法來處理這個:

  • 您可以創建一個DateFormatter子類,首先嚐試的日期 - 時間字符串格式,那麼如果失敗,嘗試純日期格式
  • 你可以給.customDate解碼策略,其中你問DecodersingleValueContainer(),解碼字串,並通過任何你想要的格式化經過解析的日期出
  • 之前,通過它您可以在周圍創建一個包裝類型提供一個定製init(from:)encode(to:)它做到這一點(但是這是不是真的比任何一個.custom戰略更好)
  • 可使用普通的字符串,你建議
  • 您可以在所有類型提供自定義init(from:)它們使用這些日期,並在那裏

所有的一切嘗試不同的東西,前兩種方法都可能會是最簡單和乾淨的 - 你會保持默認的合成實施Codable無處不在不犧牲類型安全。

+0

第一種方法是我正在尋找的方法。謝謝! – RamwiseMatt

+1

使用'Codable'看起來很奇怪,所有其他的json映射信息都是直接從相應的對象中提供的(例如,通過'CodingKeys'映射到json鍵),但日期格式是通過'JSONDecoder'爲整個DTO樹配置的。在過去使用Mantle之後,您提出的最後一個解決方案感覺是最合適的解決方案,即使它意味着爲其他字段重複許多映射代碼,否則可能會自動生成其他字段。 – fabb

2

用單個編碼器無法做到這一點。這裏最好的辦法是定製encode(to encoder:)init(from decoder:)方法,併爲這些值提供自己的翻譯,而爲另一個值留下內置的日期策略。

爲了達到此目的,可能需要考慮將一個或多個格式化程序傳遞到userInfo對象中。

14

請嘗試解碼器同樣可配置到這一點:

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 
}() 
4

面對同樣的問題,我寫了下面的擴展:

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

這個擴展可以讓你創建一個DateDecodingStrategy爲JSONDecoder該手柄同一個JSON字符串中有多種不同的日期格式。該擴展包含一個函數,該函數需要實現一個給你一個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)