How to Convert a Date String With Optional Fractional Seconds Using Codable in Swift

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

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)
}

Swift DateFormatter Optional Milliseconds

Two suggestions:

  • Convert the string with the date format including the milliseconds. If it returns nil convert it with the other format.

  • Strip the milliseconds from the string with Regular Expression:

    var dateString = "2018-01-21T20:11:20.057Z"
    dateString = dateString.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
    // -> 2018-01-21T20:11:20Z

Edit:

To use it with Codable you have to write a custom initializer, specifying dateDecodingStrategy does not work

struct Foo: Decodable {
let birthDate : Date
let name : String

private enum CodingKeys : String, CodingKey { case born, name }

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var rawDate = try container.decode(String.self, forKey: .born)
rawDate = rawDate.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
birthDate = ISO8601DateFormatter().date(from: rawDate)!
name = try container.decode(String.self, forKey: .name)
}
}

let jsonString = """
[{"name": "Bob", "born": "2018-01-21T20:11:20.057Z"}, {"name": "Matt", "born": "2018-01-21T20:11:20Z"}]
"""

do {
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode([Foo].self, from: data)
print(result)
} catch {
print("error: ", error)
}

Decoding Date values with different JSON formats in Swift?

As shown in the possible duplicate post I have already mentioned in comments, you need to create a custom date decoding strategy:

First create your date formatter for parsing the date string (note that this assumes your date is local time, if you need UTC time or server time you need to set the formatter timezone property accordingly):

extension Formatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}

Then create a custom decoding strategy to try all possible date decoding strategy you might need:

extension JSONDecoder.DateDecodingStrategy {
static let deferredORyyyyMMdd = custom {
let container = try $0.singleValueContainer()
do {
return try Date(timeIntervalSinceReferenceDate: container.decode(Double.self))
} catch {
let string = try container.decode(String.self)
if let date = Formatter.yyyyMMdd.date(from: string) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
}
}

Playground testing:

struct TestCod: Codable {
let txt: String
let date: Date
}

let jsonA = """
{
"txt":"stack",
"date":589331953.61679399
}
"""
let jsonB = """
{
"txt":"overflow",
"date":"2019-09-05"
}
"""

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .deferredORyyyyMMdd
let decodedJSONA = try! decoder.decode(TestCod.self, from: Data(jsonA.utf8))
decodedJSONA.date // "Sep 4, 2019 at 8:19 PM"
let decodedJSONB = try! decoder.decode(TestCod.self, from: Data(jsonB.utf8))
decodedJSONB.date // "Sep 5, 2019 at 12:00 AM"

How can I convert a Codable's property with type Double in a given JSON as Date using Decoder?

Instead of using the .formatted dateDecodingStrategy, you'll have to go one level deeper and use the .custom one to do the decoding yourself, converting it from an Int to a String manually:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateAsInteger = try container.decode(Int.self)
let dateAsString = "\(dateAsInteger)"
guard let date = WhateverTypeContainsCustomFormatter.customFormatter.date(from: dateAsString) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Could not form Date from value: \(dateAsString)")
}

return date
})

(Replace WhateverTypeContainsCustomFormatter with... well, whatever type of yours that contains your customFormatter.)

Unable to convert date string to Date format received through json response via JSONDecoder

I don't think you have any alternative other than creating a custom decoder for your SubModel structure:

extension SubModel {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dictionary1 = try container.decode([String:Int].self, forKey: .item1)
let dictionary2 = try container.decode([String:Int].self, forKey: .item2)
let formatter = DateFormatter()
formatter.locale = .init(identifier: "en_US_POSIX")
formatter.dateFormat = "M/d/yy"
item1 = dictionary1.reduce(into: [:], { result, kv in
guard let date = formatter.date(from: kv.key) else { return }
result[date] = kv.value
})
item2 = dictionary2.reduce(into: [:], { result, kv in
guard let date = formatter.date(from: kv.key) else { return }
result[date] = kv.value
})
}
}

I want to convert String value from API to Custom Codable Model SWIFT

Nothing really complicated about that, the item has to use a secondary JsonDecoder:

struct Item: Decodable {
let body: Body

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let jsonBody = try container.decode(String.self, forKey: .jsonBody)
let jsonBodyData = jsonBody.data(using: .utf8)!

let decoder = JSONDecoder()
body = try decoder.decode(Body.self, from: jsonBodyData)
}

private enum CodingKeys: String, CodingKey {
case jsonBody
}
}

struct Body: Decodable {
let documentInfo: ...
}

While using DateFormatter in swift error occurs

check your input Date format is wrong

    let dateFormatter: DateFormatter = DateFormatter()
//2019-08-20T08:05:15.680Z.
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
if let dateFromTimeStamp: Date = dateFormatter.date(from: "2019-08-20T08:05:15.680Z"){
dateFormatter.dateFormat = "dd MMM yyyy, hh:mm a"
if let dateString = dateFormatter.string(from: dateFromTimeStamp) as? String{
print ("dateString == \(dateString)")
}
}


Related Topics



Leave a reply



Submit