JSON date format flavours support

I have to work with backend that can return multiple flavours of iso8601 date format (and some other flavours as well). As far as I know iso8601 DateDecodingStrategy is very peculiar about format. Doing this custom strategy now:

extension DateFormatter {
    convenience init(dateFormat: String) {
        self.init()
        self.dateFormat = dateFormat
        self.locale = .init(identifier: "en_US_POSIX")
        self.calendar = .init(identifier: .gregorian)
    }
    
    static let yyyy_mm_ddThh_mm_ss = DateFormatter(dateFormat: "yyyy-MM-dd'T'HH:mm:ss")
    static let yyyy_mm_ddThh_mm_ssZ = DateFormatter(dateFormat: "yyyy-MM-dd'T'HH:mm:ssZ")
    static let yyyy_mm_ddThh_mm_ss_SSSSSSS = DateFormatter(dateFormat: "yyyy-MM-dd'T'HH:mm:ss'.'SSSSSSS")
    static let mm_d_yyyy_HH_mm_ss_PM = DateFormatter(dateFormat: "mm/d/yyyy HH:mm:ss a")
}

extension JSONDecoder.DateDecodingStrategy {
    static let custom: JSONDecoder.DateDecodingStrategy = .custom { decoder in
        let container = try decoder.singleValueContainer()
        var string = try container.decode(String.self)
        
        let formatter = DateFormatter()
        if let date = DateFormatter.yyyy_mm_ddThh_mm_ssZ.date(from: string) {
            return date
        }
        if let date = DateFormatter.yyyy_mm_ddThh_mm_ss.date(from: string) {
            return date
        }
        if let date = DateFormatter.yyyy_mm_ddThh_mm_ss_SSSSSSS.date(from: string) {
            return date
        }
        if let date = DateFormatter.mm_d_yyyy_HH_mm_ss_PM.date(from: string) {
            return date
        }
        print("TODO another format: \(string)")
        // put breakpoint here
        return Date()
    }
}

Is there a better way?

Not really.

As to your code, the formatter you allocated there seems unused. And you should produce an error if none of the formatters can parse the date rather than defaulting to the current date.

JSON date parsing is a long-standing pain point.

I've done this a few different ways. In one project I did something like your example but had only a single DateFormatter. The code loops and sets the dateFormat property in the loop and attempts to parse the string, returning the date if it's valid.

There's a third party library called SwiftDate that has a date parser that will parse all these formats and more in a single api call. I wrote a custom strategy like yours that calls SwiftDate to parse the internet dates. That was simplest for me.

There's also the ISO8601DateFormatter from Foundation. It works but has problems with fractional seconds.

There are many blogs, SO discussions and forum posts on this forum on this topic.

2 Likes

I was hoping there are some secret formatters that, say, match an arbitrary number (including zero) of "S" or optionally matching "Z", etc. Another answer I was hoping for is "use the newer date formatter API that handles that".

1 Like

When confronted with annoying date parsing problems like this I usually just skip DateFormatter entirely and write my own parser. Parsing dates in the general case is super hard. Parsing fixed-format dates like this is easy, and once I have a set of date components I can use Calendar to turn that into a Date.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes