Swift\'s JSONDecoder
offers a dateDecodingStrategy
property, which allows us to define how to interpret incoming date strings in accordance with a
There are a few ways to deal with this:
DateFormatter
subclass which first attempts the date-time string format, then if it fails, attempts the plain date format.custom
Date
decoding strategy wherein you ask the Decoder
for a singleValueContainer()
, decode a string, and pass it through whatever formatters you want before passing the parsed date outDate
type which provides a custom init(from:)
and encode(to:)
which does this (but this isn't really any better than a .custom
strategy)init(from:)
on all types which use these dates and attempt different things in thereAll in all, the first two methods are likely going to be the easiest and cleanest — you'll keep the default synthesized implementation of Codable
everywhere without sacrificing type safety.
Facing this same issue, I wrote the following extension:
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)")
}
})
}
}
This extension allows you to create a DateDecodingStrategy for the JSONDecoder that handles multiple different date formats within the same JSON String. The extension contains a function that requires the implementation of a closure that gives you a CodingKey, and it is up to you to provide the correct DateFormatter for the provided key.
Lets say that you have the following JSON:
{
"publication_date": "2017-11-02",
"opening_date": "2017-11-03",
"date_updated": "2017-11-08 17:45:14"
}
The following Struct:
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"
}
}
Then to decode the JSON, you would use the following code:
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)
It is a little verbose, but more flexible approach: wrap date with another Date class, and implement custom serialize methods for it. For example:
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")
}
}
}
So now you are independent of .dateDecodingStrategy
and .dateEncodingStrategy
and your MyCustomDate
dates will parsed with specified format. Use it in class:
class User: Codable {
var dob: MyCustomDate
}
Instantiate with
user.dob = MyCustomDate(date)
Please try decoder configurated similarly to this:
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
}()
If you have multiple dates with different formats in single model, its bit difficult to apply .dateDecodingStrategy
for each dates.
Check here https://gist.github.com/romanroibu/089ec641757604bf78a390654c437cb0 for a handy solution
try this. (swift 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
}