I'm dealing with API that returns dates in a mixture of formats, with or without fractional seconds of various lengths and with or without Z. To parse it from JSON I'm doing:
let decoder = JSONDecoder()
let f1 = DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ss")
let f2 = DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ssZ")
let f3 = DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ss.SSSZ")
let f4 = DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ss.SSSSSS")
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let s = try container.decode(String.self)
let date = f1.date(from: s) ?? f2.date(from: s) ?? f3.date(from: s) ?? f4.date(from: s)!
// throwing instead in real code
return date
})
Once I hit a new use case during testing I'm adding another formatter to the above list.
I wonder if there's more modern and/or straightforward way?
Not that I’m aware of. I’m flabbergasted that Foundation does not come with a ready-made parser that accepts multiple formats. That’s a pain point in 100% of apps I work on, because of course servers are not consistent in their date formatting. But why should they? "Be liberal in what you accept, and conservative in what you send" used to be a living principle. But no, we have to try up to two, three, four, five parsers, until one disdainfully passes, projecting on me its poor jugement on the quality on a server API I do not control. Give me a break, Foundation, and parse this date already!
One way to make it less painful to extend is to use an array instead of a bunch of lets:
let formatters = [
DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ss"),
DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ssZ"),
DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ss.SSSZ"),
DateFormatter(dateFormat: "YYYY-MM-DD'T'HH:mm:ss.SSSSSS")
]
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let s = try container.decode(String.self)
for formatter in formatters {
if let date = formatter.date(from: s) {
return date
}
}
throw ...
})
FYI, most formatters, but especially DateFormatter, should be cached, usually as static instances, as they're expensive to create. If you're using them in an array like this you can make the array a static value.
@gwendal.roue This is what ISO8601DateFormattershould support, but it's more like OneSingleISO8601FormatDateFormatter, in that it doesn't dynamically support the possible 8601 formats. You can express these formats as separate ISO8601DateFormatter instances pretty easily (just set the corresponding formatOptions) but I don't think that buys you anything over manual formatters.
If you're using Swift 6, sendability rules would beg to differ. I was doing something similar to this in a production codebase, and relatively recently changed it to use [Date.ISO8601FormatStyle] instead of [DateFormatter] for exactly this reason.
I think the idea that Date is Codable, and the idea that JSONDecoder has a date format, is simply a misfeature.
I wish there was a general feature to provide custom encode/decode implementations for types that don't have a single canonical encoded representation.
struct ShortSlashUTCDateFormat: DateTransformer {
static let formatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "dd/MM/yyyy"
df.calendar = Calendar(identifier: .gregorian)
df.timeZone = TimeZone(secondsFromGMT: 0)!
return df
}()
}
struct IHaveADate: Codable, Equatable {
@TransformCoding<ShortSlashUTCDateFormat>
var date: Date
}
(this is specifically for dates as a special case, but the library supports other types too).
Now each field in your API types just needs to know what format the server uses for that particular field, you don't have to have consistency across the whole API.
Yeah, I was leaning towards the same idea. For example JSON could be constructed from several parts that were implemented by different teams, one part of it uses integer milliseconds since 1970, the other float seconds since 2000 and yet another date strings of a particular flavour. My thought-experiment implementation would be using something like this
enum DateFormatVariant {
case builtin
case int(multiplier: Int, zero: Date)
case double(multiplier: Double, zero: Date)
case string(dateFormat: String)
case customEncoder((Encoder, Date) throws -> Void)
case customDecoder((Decoder) throws -> Date)
static let secondsSince1970 = Self.double(multiplier: 1, zero: .init(timeIntervalSince1970: 0))
...
}
to specify various flavours of dates, with a protocol that particular ready-made and custom formatters are conforming to:
protocol DateFormat {
static var encodingFormat: DateFormatVariant { get }
static var decodingFormats: [DateFormatVariant] { get }
// very good reason to have this as an array instead of a set
}
Example conforming formatter:
enum BuiltinFormatter: DateFormat {
static let encodingFormat: DateFormatVariant = .builtin
static let decodingFormats: [DateFormatVariant] = [.builtin]
}
The individual formatters would be as strict (or as liberal) as they want during decoding.
Any API that does that is malformed and not worth using.
Creating multiple parsing formatters to deal with the fact that ISO8601DateFormatter doesn't actually parse ISO8601 dates is one thing, but not even the worst APIs I've used have had such malformed dates.