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?
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.
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.
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 .
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 .
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) }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With