Iso8601 Date JSON Decoding Using Swift4

iso8601 date json decoding using swift4

You can use like this :

enum DateError: String, Error {
case invalidDate
}

let decoder = JSONDecoder()

let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)

decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)

formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
if let date = formatter.date(from: dateStr) {
return date
}
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
if let date = formatter.date(from: dateStr) {
return date
}
throw DateError.invalidDate
})

Decoding JSON data in swift4 with dates

A date is a Double in that it is the number of ms since the epoch.

Try this:

let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
// Use format appropriate to your JSON String. This is for ISO-8601
// You MUST account for the milliseconds even if you don't want them
// or it won't parse properly
dateFormatter.dateFormat = "yy-MM-dd'T'HH:mm:ss.SSS"

eventDetail.EventStartDate = dateFormatter.date(from: jsonEventStartDateField)!
eventDetail.EventEndDate = dateFormatter.date(from: jsonEventEndDateField)!

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 ISO 8601 date formatting

import Foundation
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let theDate = dateFormatter.date(from: "1993-07-20T09:44:18.674Z")!
let newDateFormater = DateFormatter()
newDateFormater.dateFormat = "dd-MMM-yyyy"
print(newDateFormater.string(from: theDate))

First convert the string to date using proper date format. Then convert it back to string using the format you want.

How can I parse / create a date time stamp formatted with fractional seconds UTC timezone (ISO 8601, RFC 3339) in Swift?

Swift 4 • iOS 11.2.1 or later

extension ISO8601DateFormatter {
convenience init(_ formatOptions: Options) {
self.init()
self.formatOptions = formatOptions
}
}


extension Formatter {
static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
}


extension Date {
var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
}


extension String {
var iso8601withFractionalSeconds: Date? { return Formatter.iso8601withFractionalSeconds.date(from: self) }
}

Usage:

Date().description(with: .current)  //  Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
let dateString = Date().iso8601withFractionalSeconds // "2019-02-06T00:35:01.746Z"

if let date = dateString.iso8601withFractionalSeconds {
date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
print(date.iso8601withFractionalSeconds) // "2019-02-06T00:35:01.746Z\n"
}

iOS 9 • Swift 3 or later

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


Codable Protocol

If you need to encode and decode this format when working with Codable
protocol you can create your own custom date encoding/decoding strategies:

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

and the encoding strategy

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

Playground Testing

let dates = [Date()]   // ["Feb 8, 2019 at 9:48 PM"]

encoding

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
let data = try! encoder.encode(dates)
print(String(data: data, encoding: .utf8)!)

decoding

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
let decodedDates = try! decoder.decode([Date].self, from: data) // ["Feb 8, 2019 at 9:48 PM"]

Sample Image

change date format iso-8601 to custom format

When you are decoding a Date the decoder expects an UNIX timestamp (a Double) by default, this is what the error message tells you.

However you can indeed decode an ISO8601 string as Date if you add the decoder.dateDecodingStrategy = .iso8601 but this decodes only standard ISO8601 strings without milliseconds.

There are two options:

  1. Add a formatted dateDecodingStrategy with a DateFormatter.

    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(dateFormatter)
    try decoder.decode(...
  2. Declare timestamp as

    let timestamp: String

    and convert the string back and forth with two formatters or two date formats in dateString.

Swift's JSONDecoder with multiple date formats in a JSON string?

There are a few ways to deal with this:

  • You can create a DateFormatter subclass which first attempts the date-time string format, then if it fails, attempts the plain date format
  • You can give a .custom Date decoding strategy wherein you ask the Decoder for a singleValueContainer(), decode a string, and pass it through whatever formatters you want before passing the parsed date out
  • You can create a wrapper around the Date type which provides a custom init(from:) and encode(to:) which does this (but this isn't really any better than a .custom strategy)
  • You can use plain strings, as you suggest
  • You can provide a custom init(from:) on all types which use these dates and attempt different things in there

All in all, the first two methods are likely going to be the easiest and cleanest — you'll keep the default synthesized implementation of Codable everywhere without sacrificing type safety.



Related Topics



Leave a reply



Submit