swiftlang / swift-corelibs-foundation

The Foundation Project, providing core utilities, internationalization, and OS independence
swift.org
Apache License 2.0
5.29k stars 1.13k forks source link

[SR-5823] ISO8601 DateFormatter/Codeable incomplete #4073

Open swift-ci opened 7 years ago

swift-ci commented 7 years ago
Previous ID SR-5823
Radar rdar://problem/32755663
Original Reporter FGoessler (JIRA User)
Type Bug
Additional Detail from JIRA | | | |------------------|-----------------| |Votes | 4 | |Component/s | Foundation | |Labels | Bug | |Assignee | @itaiferber | |Priority | Medium | md5: 6cc2a18bb37f3c5b8856aeac7a543bba

Issue Description:

The ISO8601 DateFormatter which is apparently also used for the Codeable dateDecodingStrategy/dateEncodingStrategy iso8601 does not parse/validate all ISO8601 dates as expected.
It especially does not accept iso8601 dates with sub seconds.

Expected:

2017-09-03T10:08:00Z => valid iso8601 date
2017-09-03T10:08:00.000Z => valid iso8601 date

Actual:

2017-09-03T10:08:00Z => valid iso8601 date
2017-09-03T10:08:00.000Z => invalid iso8601 date

Minimal Sample Code:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
try decoder.decode([Date].self, from: "["2017-09-03T10:08:00Z"]".data(using: .utf8)!) // works
try decoder.decode([Date].self, from: "["2017-09-03T10:08:00.000Z"]".data(using: .utf8)!) // throws

Version Info:

Used Xcode 9 Beta 6 in iOS 11 Simulator on macOS Sierra 10.12.6.

Note:

I discovered this while consuming an REST API that is implemented in JavaScript (node.js). When creating a Date object in JavaScript and transforming it to an iso string using "toISOString" (e.g. "(new Date()).toISOString()") it creates a date string including sub-seconds (e.g. "2017-09-03T10:08:00.000Z").
This means that one cannot consume a "standard" JavaScript API with Swift's builtin Codable date parser and need to create a custom DateFormatter (e.g. with the pattern: "yyyy-MM-dd'T'HH:mm:ss'.'SSSZZZZZ").

This behavior is really inconvenient and a great bummer for Swift's Codeable support.

I'm not an expert on date formats but I expected this to work and it took me some time to dig through the differences why this did not work. Would be great if we could save this time waste from future developers.

belkadan commented 7 years ago

cc @itaiferber

itaiferber commented 7 years ago

This is a known issue — ISO8601DateFormatter does not currently support sub-second precision, but this will be added in a future release of Swift.

swift-ci commented 5 years ago

Comment by Maurice Arikoglu (JIRA)

@itaiferber ISO8601DateFormatter supports sub-second precision in the meantime, this inconsistency still exists though. Has the time come to tackle this maybe?

itaiferber commented 5 years ago

arim (JIRA User) Unfortunately, we haven't had the bandwidth to dedicate to expanding the API to support this — because ISO8601DateFormatter is strict about the values it supports, we cannot change the behavior of .iso8601 to add subsecond parsing because that will break existing use cases of strings which don't have fractional seconds.

Instead, we'd like to add an .iso8601WithOptions(ISO8601DateFormatter.Options) option, but this requires going through API review first. It's still an open task that we'd like to resolve.

swift-ci commented 5 years ago

Comment by Basem Emara (JIRA)

I really like the proposed solution of doing this:

JSONDecoder().dateDecodingStrategy = .iso8601WithOptions(ISO8601DateFormatter.Options)

To me this is an oversight with the JSONDecoder because I'd imagine the majority of ISO8601 use cases is with JSON. Unless we use a custom strategy and get messy with `.custom`, we can't use the `ISO8601DateFormatter` and `JSONDecoder` together which seem like a natural combination.

mxcl commented 5 years ago

I will happily submit the PR for this if someone from Swift core can make a decision on how it should be fixed. Need it for my current work.

itaiferber commented 5 years ago

The issue is not a decision about how it should work, but about the bandwidth to get things through API review internally. In the meantime, you can work around this by creating a date formatter with the expected format (including sub-second precision) and apply it using the JSONDecoder.DateDecodingStrategy.formatted strategy.

swift-ci commented 5 years ago

Comment by Maurice Arikoglu (JIRA)

@Itai Ferber I have opened another bug with the ISO date formatter. Maybe this could be fixed with the same proposed solution: #SR-9266

swift-ci commented 5 years ago

Comment by Maurice Arikoglu (JIRA)

@itaiferber

swift-ci commented 4 years ago

Comment by Tanner Stirrat (JIRA)

We just ran into this problem today. Any updates here? We're currently hacking around it on our backend, but that's a less than ideal solution, to put it lightly.

swift-ci commented 4 years ago

Comment by Oswaldo Rubio (JIRA)

Same here, I don´t like to use the custom decoding strategy and having to iterate each iso8601 variant.

kurtbuilds commented 1 year ago
extension JSONDecoder.DateDecodingStrategy {
    static let iso8601WithSeconds = custom {
        let container = try $0.singleValueContainer()
        let string = try container.decode(String.self)
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        guard let date = formatter.date(from: string) else {
            throw DecodingError.dataCorruptedError(in: container,
                  debugDescription: "Invalid date in iso8601withSeconds: " + string)
        }
        return date
    }
}

gives a .iso8601WithSeconds variant that solves this issue. (Swift 5.5)

Use like

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