Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to convert a date string with optional fractional seconds using Codable in Swift4

I am replacing my old JSON parsing code with Swift's Codable and am running into a bit of a snag. I guess it isn't as much a Codable question as it is a DateFormatter question.

Start with a struct

 struct JustADate: Codable {     var date: Date  } 

and a json string

let json = """   { "date": "2017-06-19T18:43:19Z" } """ 

now lets decode

let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601  let data = json.data(using: .utf8)! let justADate = try! decoder.decode(JustADate.self, from: data) //all good 

But if we change the date so that it has fractional seconds, for example:

let json = """   { "date": "2017-06-19T18:43:19.532Z" } """ 

Now it breaks. The dates sometimes come back with fractional seconds and sometimes do not. The way I used to solve it was in my mapping code I had a transform function that tried both dateFormats with and without the fractional seconds. I am not quite sure how to approach it using Codable however. Any suggestions?

like image 441
Guillermo Alvarez Avatar asked Sep 27 '17 23:09

Guillermo Alvarez


People also ask

What is Codable and Codable in Swift?

Codable; the data-parsing dream come true!Codable is the combined protocol of Swift's Decodable and Encodable protocols. Together they provide standard methods of decoding data for custom types and encoding data to be saved or transferred.

How do you string convert in to date in Swift?

Show activity on this post. let isoDate = "2016-04-14T10:44:00+0000" let dateFormatter = DateFormatter() dateFormatter. locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX dateFormatter. dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" let date = dateFormatter.

Is string Codable in Swift?

Remember, Swift's String , Int , and Bool are all Codable ! Earlier I wrote that your structs, enums, and classes can conform to Codable . Swift can generate the code needed to extract data to populate a struct's properties from JSON data as long as all properties conform to Codable .

How do you make Codable?

The simplest way to make a type codable is to declare its properties using types that are already Codable . These types include standard library types like String , Int , and Double ; and Foundation types like Date , Data , and URL .


1 Answers

You can use two different date formatters (with and without fraction seconds) and create a custom DateDecodingStrategy. In case of failure when parsing the date returned by the API you can throw a DecodingError as suggested by @PauloMattos in comments:

iOS 9, macOS 10.9, tvOS 9, watchOS 2, Xcode 9 or later

The custom ISO8601 DateFormatter:

extension Formatter {     static let iso8601withFractionalSeconds: DateFormatter = {         let formatter = DateFormatter()         formatter.calendar = Calendar(identifier: .iso8601)         formatter.locale = Locale(identifier: "en_US_POSIX")         formatter.timeZone = TimeZone(secondsFromGMT: 0)         formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"         return formatter     }()     static let iso8601: DateFormatter = {         let formatter = DateFormatter()         formatter.calendar = Calendar(identifier: .iso8601)         formatter.locale = Locale(identifier: "en_US_POSIX")         formatter.timeZone = TimeZone(secondsFromGMT: 0)         formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"         return formatter     }() } 

The custom DateDecodingStrategy:

extension JSONDecoder.DateDecodingStrategy {     static let customISO8601 = custom {         let container = try $0.singleValueContainer()         let string = try container.decode(String.self)         if let date = Formatter.iso8601withFractionalSeconds.date(from: string) ?? Formatter.iso8601.date(from: string) {             return date         }         throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")     } } 

The custom DateEncodingStrategy:

extension JSONEncoder.DateEncodingStrategy {     static let customISO8601 = custom {         var container = $1.singleValueContainer()         try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))     } } 

edit/update:

Xcode 10 • Swift 4.2 or later • iOS 11.2.1 or later

ISO8601DateFormatter now supports formatOptions .withFractionalSeconds:

extension Formatter {     static let iso8601withFractionalSeconds: ISO8601DateFormatter = {         let formatter = ISO8601DateFormatter()         formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]         return formatter     }()     static let iso8601: ISO8601DateFormatter = {         let formatter = ISO8601DateFormatter()         formatter.formatOptions = [.withInternetDateTime]         return formatter     }() } 

The customs DateDecodingStrategy and DateEncodingStrategy would be the same as shown above.


// Playground testing struct ISODates: Codable {     let dateWith9FS: Date     let dateWith3FS: Date     let dateWith2FS: Date     let dateWithoutFS: Date } 

let isoDatesJSON = """ { "dateWith9FS": "2017-06-19T18:43:19.532123456Z", "dateWith3FS": "2017-06-19T18:43:19.532Z", "dateWith2FS": "2017-06-19T18:43:19.53Z", "dateWithoutFS": "2017-06-19T18:43:19Z", } """ 

let isoDatesData = Data(isoDatesJSON.utf8)  let decoder = JSONDecoder() decoder.dateDecodingStrategy = .customISO8601  do {     let isoDates = try decoder.decode(ISODates.self, from: isoDatesData)     print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith9FS))   // 2017-06-19T18:43:19.532Z     print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith3FS))   // 2017-06-19T18:43:19.532Z     print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWith2FS))   // 2017-06-19T18:43:19.530Z     print(Formatter.iso8601withFractionalSeconds.string(from: isoDates.dateWithoutFS)) // 2017-06-19T18:43:19.000Z } catch {     print(error) } 
like image 156
Leo Dabus Avatar answered Oct 09 '22 09:10

Leo Dabus