Although, 2017-09-04T04:14:37.000Z is a valid ISO8601-formatted date, ISO8601DateFormatter cannot parse it. Seemingly, it happens when the string that represents an ISO8601-formatted date contains fractions of a second:
let formatter = ISO8601DateFormatter()
let ret1 = formatter.date(from: "2017-09-04T04:14:37.000Z"); // ret1 is null
let ret2 = formatter.date(from: "2017-09-04T04:14:37Z"); // ret2 is not null
I tracked down the issue and found that under the hood withInternetDateTime is used for formatOption and according to the documentation it doesn't support seconds fractions.
fileprivate var _iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}()
There is another option .withFractionalSeconds which allows parsing when fractional seconds are included. (use [.withInternetDateTime, .withFractionalSeconds])
That being said, it is then not parsing the one without fractional seconds
I have experienced the same issue. ISO8601DateFormatter does not support full ISO08601 with regard to fractional seconds. The way I've worked around it is by using a .custom.dateDecodingStrategy on JSONDecoder that attempts to parse the date through a chain of formatters.
That is a very inefficient way of doing this I guess.
If you know at least that certain properties have certain formats, you can use a custom strategy and switch on the current key.
This is what I do too. I created this post to bring up the issue to see if there is any reasonable explanation for not supporting seconds fractions or not. Otherwise, we should fix it.
I don't think I could assume that certain properties would have guaranteed format. After all, they were all ISO08601, it just that sometimes fractional seconds were missing.
But I agree, switch would be more efficient if possible.
Is there any progress on this subject?
I am having this issue where I send an ISO8601 date which is not always parsed.
When looking at the ISO8601 definitions at Date and Time Formats ,
there are many different allowed formats:
The formats are as follows. Exactly the components shown here must be present, with exactly this punctuation. Note that the "T" appears literally in the string, to indicate the beginning of the time element, as specified in ISO 8601.
Year:
YYYY (eg 1997)
Year and month:
YYYY-MM (eg 1997-07)
Complete date:
YYYY-MM-DD (eg 1997-07-16)
Complete date plus hours and minutes:
YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
Complete date plus hours, minutes and seconds:
YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
Complete date plus hours, minutes, seconds and a decimal fraction of a
second
YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
where:
YYYY = four-digit year
MM = two-digit month (01=January, etc.)
DD = two-digit day of month (01 through 31)
hh = two digits of hour (00 through 23) (am/pm NOT allowed)
mm = two digits of minute (00 through 59)
ss = two digits of second (00 through 59)
s = one or more digits representing a decimal fraction of a second
TZD = time zone designator (Z or +hh:mm or -hh:mm)
in particular: "s = one or more digits representing a decimal fraction of a second"
Yes, the lack of turning on the option for fractional seconds on ISO8601DateFormatter is a pain point for me when parsing JSON. You can make your own DateFormatter that supports fractional seconds. I've used SwiftDate's ISO Parser for this also.
I don't know why this option has been left off but it seems like a bug or bad decision to do it that way.
A Profile of ISO 8601 is a specification developed by a particular community which explains how ISO 8601 is to be used, to carry out a particular function or group of functions relevant to that community. link
So in Swift we are using an implementation of RFC3339.
OK, but why doesn't JSONDecoder support fractions of a second?
In section 5.6 of RFC3339, you can find the exact specification of a valid Internet Date/Time format and in this specification time-secfrac is optional:
This part of RFC may justify why they decided not to support this optional part:
5.3. Rarely Used Options
...
The format defined below includes only one rarely used option:
fractions of a second. It is expected that this will be used only by
applications which require strict ordering of date/time stamps or
which have an unusual precision requirement.
Though, based on my day-to-day experience fractions of a second is not rare at all.
Is there a way to make the world a better place?
I mean: how can we improve the behaviour of this? Can we create a bug report/feature request, or do we have to keep writing our own duplicates of built in classes because the built in classes are sub-optimal?
Kind regards,
Wouter
AFAICS, the solution is simple. We just need to change one line of JSONEncoder.swift.
Where the _iso8601Formatter is being created:
private var _iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}()
needs to be changed to:
private var _iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
But by this change 'fractions of second' becomes mandatory. A workaround could be adding a associated value to .iso8601 case where we can specify if withFractionalSeconds must be included in formatOptions or not. A less efficient solution is trying to decode data without .withFractionalSeconds first and if the result is nil, trying again but this time with .withFractionalSeconds. Also, we can add a new case to DateEncodingStrategy, .iso8601FractionalSeconds.
Any way, I guess the right direction is to pitch a proposal in this forum.
The Foundation API is an Apple product not subject to the Swift Evolution process except as it impacts the Swift compiler, standard library, or other components of the Swift project.
That's not true — although the base Encoder and Decoder protocol types are in the stdlib, JSONDecoder and its sibling PropertyListDecoder (and the encoder variants) live in Foundation and are subject to Foundation evolution, separately from stdlib evolution.
When I worked on JSONDecoder, I filed a Radar away for adding .iso8601WithOptions(...) which would allow you to specify the ISO8601DateFormatter options you want. Although it's a tiny change, it's possibly too small of a change to spin up the whole API review process for. I had always planned on bundling that in with other JSONDecoder changes, but didn't get a chance to.
I would recommend filing Feedback to add visibility to this; although it will likely get duped to the original Radar, this is still a nice QoL change that I think is worth making.
When encoding iso8601 dates you might want careful control of the format. When decoding iso8601 dates I think you want a lenient parser, especially if you're writing a parser that will be the default for all users of JSONDecoder. It's frankly bizarre that JSONDecoder uses seconds as the default format for Date instead of the iso8601 format. Seconds might make sense for how to encode/decode Date in general but not for JSON.
Aside from the fractional seconds issue when decoding iso8601 dates I've also run into an issue with dash vs colon in the time zone specifier. SwiftDate's ISOParser is lenient and accepts all these variants so for now I use a custom formatter for decoding iso8601 dates in JSON that's based on SwiftDate.
Date parsing is one of the bigger pain points in using Decodable.
You can also try my library, JJLISO8601DateFormatter, that's a drop-in replacement for ISO8601DateFormatter and is ~10x faster. Changes like this get quickly merged, so feel free to file an issue.
Based on Date and Time Formats the desired format must have fractional seconds or not, therefore a parsers need not to handle both styles. So, assuming each API follows a different standard, the only thing you would need to do is to specify a different parsing strategy for each API.
The formats are as follows. Exactly the components shown here must be present, with exactly this punctuation.
s = one or more digits representing a decimal fraction of a second
An adopting standard that permits fractions of a second must specify both the minimum number of digits (a number greater than or equal to one)
Based on that it's understandable why ISO8601DateFormatter works the way it does. Is it strict? Yes, but so is the ISO8601 spec.