Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 522 Vote(s) - 3.48 Average
  • 1
  • 2
  • 3
  • 4
  • 5
How to convert a date string with optional fractional seconds using Codable in Swift?

#1
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?
Reply

#2
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][1] 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)
}

[1]:

[To see links please register here]

Reply

#3
#Swift 5

To parse ISO8601 string to date you have to use DateFormatter. In newer systems (f.ex. iOS11+) you can use ISO8601DateFormatter.

As long as you don't know if date contains milliseconds, you should create 2 formatters for each case. Then, during parsing String to Date use both consequently.

##DateFormatter for older systems

/// Formatter for ISO8601 with milliseconds
lazy var iso8601FormatterWithMilliseconds: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"

return dateFormatter
}()

/// Formatter for ISO8601 without milliseconds
lazy var iso8601Formatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"

return dateFormatter
}()

## ISO8601DateFormatter for newer systems (f.ex. iOS 11+)

lazy var iso8601FormatterWithMilliseconds: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()

// GMT or UTC -> UTC is standard, GMT is TimeZone
formatter.timeZone = TimeZone(abbreviation: "GMT")
formatter.formatOptions = [.withInternetDateTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime,
.withTimeZone,
.withFractionalSeconds]

return formatter
}()

/// Formatter for ISO8601 without milliseconds
lazy var iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()

// GMT or UTC -> UTC is standard, GMT is TimeZone
formatter.timeZone = TimeZone(abbreviation: "GMT")
formatter.formatOptions = [.withInternetDateTime,
.withDashSeparatorInDate,
.withColonSeparatorInTime,
.withTimeZone]

return formatter
}()

##Summary

As you can notice there is 2 formatters to create. If you want to support older systems, it makes 4 formatters. To make it more simple, check out [Tomorrow on GitHub](

[To see links please register here]

) where you can see entire solution for this problem.

To convert String to Date you use:

`let date = Date.fromISO("2020-11-01T21:10:56.22+02:00")`


Reply

#4
A new option (as of Swift 5.1) is a Property Wrapper. The [CodableWrappers][1] library has an easy way to deal with this.

For default ISO8601
```swift
@ISO8601DateCoding
struct JustADate: Codable {
var date: Date
}
```
If you want a custom version:
```
// Custom coder
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
public struct FractionalSecondsISO8601DateStaticCoder: StaticCoder {

private static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withFractionalSeconds
return formatter
}()

public static func decode(from decoder: Decoder) throws -> Date {
let stringValue = try String(from: decoder)
guard let date = iso8601Formatter.date(from: stringValue) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
}
return date
}

public static func encode(value: Date, to encoder: Encoder) throws {
try iso8601Formatter.string(from: value).encode(to: encoder)
}
}
// Property Wrapper alias
public typealias ISO8601FractionalDateCoding = CodingUses<FractionalSecondsISO8601DateStaticCoder>

// Usage
@ISO8601FractionalDateCoding
struct JustADate: Codable {
var date: Date
}
```

[1]:

[To see links please register here]

Reply

#5
Alternatively to @Leo's answer, and if you need to provide support for older OS'es (`ISO8601DateFormatter` is available only starting with iOS 10, mac OS 10.12), you can write a custom formatter that uses both formats when parsing the string:

class MyISO8601Formatter: DateFormatter {

static let formatters: [DateFormatter] = [
iso8601Formatter(withFractional: true),
iso8601Formatter(withFractional: false)
]

static func iso8601Formatter(withFractional fractional: Bool) -> 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\(fractional ? ".SSS" : "")XXXXX"
return formatter
}

override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
guard let date = (type(of: self).formatters.flatMap { $0.date(from: string) }).first else {
error?.pointee = "Invalid ISO8601 date: \(string)" as NSString
return false
}
obj?.pointee = date as NSDate
return true
}

override public func string(for obj: Any?) -> String? {
guard let date = obj as? Date else { return nil }
return type(of: self).formatters.flatMap { $0.string(from: date) }.first
}
}
, which you can use it as date decoding strategy:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())

Although a little bit uglier in implementation, this has the advantage of being consistent with the decoding errors that Swift throws in case of malformed data, as we don't alter the error reporting mechanism).

For example:

struct TestDate: Codable {
let date: Date
}

// I don't advocate the forced unwrap, this is for demo purposes only
let jsonString = "{\"date\":\"2017-06-19T18:43:19Z\"}"
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
do {
print(try decoder.decode(TestDate.self, from: jsonData))
} catch {
print("Encountered error while decoding: \(error)")
}

will print `TestDate(date: 2017-06-19 18:43:19 +0000)`

Adding the fractional part

let jsonString = "{\"date\":\"2017-06-19T18:43:19.123Z\"}"
will result in the same output: `TestDate(date: 2017-06-19 18:43:19 +0000)`

However using an incorrect string:

let jsonString = "{\"date\":\"2017-06-19T18:43:19.123AAA\"}"
will print the default Swift error in case of incorrect data:

Encountered error while decoding: dataCorrupted(Swift.DecodingError.Context(codingPath: [__lldb_expr_84.TestDate.(CodingKeys in _B178608BE4B4E04ECDB8BE2F689B7F4C).date], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through