36

So, I've got iso8601 dates in my json which look like "2016-06-07T17:20:00.000+02:00"

Is there a way to parse these iso8601 dates using swift4? Am I missing something obvious?

I tried the following, but only the dateString "2016-06-07T17:20:00Z" from jsonShipA is parsable....

import Foundation

struct Spaceship : Codable {
    var name: String
    var createdAt: Date
}

let jsonShipA = """
{
    "name": "Skyhopper",
    "createdAt": "2016-06-07T17:20:00Z"
}
"""

let jsonShipB = """
{
    "name": "Skyhopper",
    "createdAt": "2016-06-07T17:20:00.000+02:00"
}
"""

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

let dataA = jsonShipA.data(using: .utf8)!
if let decodedShip = try? decoder.decode(Spaceship.self, from: dataA) {
    print("jsonShipA date = \(decodedShip.createdAt)")
} else {
    print("Failed to decode iso8601 date format from jsonShipA")
}

let dataB = jsonShipB.data(using: .utf8)!
if let decodedShip = try? decoder.decode(Spaceship.self, from: dataB) {
    print("jsonShipA date = \(decodedShip.createdAt)")
} else {
    print("Failed to decode iso8601 date format from jsonShipB")
}

The output of the playground is:

jsonShipA date = 2016-06-07 17:20:00 +0000
Failed to decode iso8601 date format from jsonShipB

The error being thrown is "Expected date string to be ISO8601-formatted." But to my knowledge, the date "2016-06-07T17:20:00.000+02:00" is a valid ISO8601 date

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
Tycho Pandelaar
  • 7,367
  • 8
  • 44
  • 70

2 Answers2

62

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
})
Jon Willis
  • 6,993
  • 4
  • 43
  • 51
Vini App
  • 7,339
  • 2
  • 26
  • 43
54

TL;DR version: it only parses the withInternetDateTime format of the ISO8601DateFormatter described here. This means that your string should not have milliseconds.

More info:

Looking at the Swift source on line 787, the comment says:

/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).

Looking at that RFC, it gives a couple of (admittedly tricky) examples in section 5.8:

1985-04-12T23:20:50.52Z
1996-12-19T16:39:57-08:00
1996-12-20T00:39:57Z
1990-12-31T23:59:60Z
1990-12-31T15:59:60-08:00
1937-01-01T12:00:27.87+00:20

Only the second and the third example are actually decoded by Swift, the rest fails. It seems to me that either the comment is incorrect, or the implementation is not complete. As for the real implementation, that's outside the Swift source, it simply seems to use the ISO8601DateFormatter class in Foundation.

The Swift unittest is also very limited, see line 180. It simply encodes a single date, and then decodes it back. So in other words, the only thing that's tested, is the format that the ISO8601DateFormatter outputs by default, which is hardcoded to the option .withInternetDateTime, described here.

quietNaf
  • 63
  • 7
Bart van Kuik
  • 4,704
  • 1
  • 33
  • 57