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 DateFormatter Optional Milliseconds
Two suggestions:
Convert the string with the date format including the milliseconds. If it returns
nil
convert it with the other format.Strip the milliseconds from the string with Regular Expression:
var dateString = "2018-01-21T20:11:20.057Z"
dateString = dateString.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
// -> 2018-01-21T20:11:20Z
Edit:
To use it with Codable
you have to write a custom initializer, specifying dateDecodingStrategy
does not work
struct Foo: Decodable {
let birthDate : Date
let name : String
private enum CodingKeys : String, CodingKey { case born, name }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var rawDate = try container.decode(String.self, forKey: .born)
rawDate = rawDate.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
birthDate = ISO8601DateFormatter().date(from: rawDate)!
name = try container.decode(String.self, forKey: .name)
}
}
let jsonString = """
[{"name": "Bob", "born": "2018-01-21T20:11:20.057Z"}, {"name": "Matt", "born": "2018-01-21T20:11:20Z"}]
"""
do {
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode([Foo].self, from: data)
print(result)
} catch {
print("error: ", error)
}
Decoding Date values with different JSON formats in Swift?
As shown in the possible duplicate post I have already mentioned in comments, you need to create a custom date decoding strategy:
First create your date formatter for parsing the date string (note that this assumes your date is local time, if you need UTC time or server time you need to set the formatter timezone property accordingly):
extension Formatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}
Then create a custom decoding strategy to try all possible date decoding strategy you might need:
extension JSONDecoder.DateDecodingStrategy {
static let deferredORyyyyMMdd = custom {
let container = try $0.singleValueContainer()
do {
return try Date(timeIntervalSinceReferenceDate: container.decode(Double.self))
} catch {
let string = try container.decode(String.self)
if let date = Formatter.yyyyMMdd.date(from: string) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
}
}
Playground testing:
struct TestCod: Codable {
let txt: String
let date: Date
}
let jsonA = """
{
"txt":"stack",
"date":589331953.61679399
}
"""
let jsonB = """
{
"txt":"overflow",
"date":"2019-09-05"
}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .deferredORyyyyMMdd
let decodedJSONA = try! decoder.decode(TestCod.self, from: Data(jsonA.utf8))
decodedJSONA.date // "Sep 4, 2019 at 8:19 PM"
let decodedJSONB = try! decoder.decode(TestCod.self, from: Data(jsonB.utf8))
decodedJSONB.date // "Sep 5, 2019 at 12:00 AM"
How can I convert a Codable's property with type Double in a given JSON as Date using Decoder?
Instead of using the .formatted
dateDecodingStrategy
, you'll have to go one level deeper and use the .custom
one to do the decoding yourself, converting it from an Int
to a String
manually:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateAsInteger = try container.decode(Int.self)
let dateAsString = "\(dateAsInteger)"
guard let date = WhateverTypeContainsCustomFormatter.customFormatter.date(from: dateAsString) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Could not form Date from value: \(dateAsString)")
}
return date
})
(Replace WhateverTypeContainsCustomFormatter
with... well, whatever type of yours that contains your customFormatter
.)
Unable to convert date string to Date format received through json response via JSONDecoder
I don't think you have any alternative other than creating a custom decoder for your SubModel structure:
extension SubModel {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dictionary1 = try container.decode([String:Int].self, forKey: .item1)
let dictionary2 = try container.decode([String:Int].self, forKey: .item2)
let formatter = DateFormatter()
formatter.locale = .init(identifier: "en_US_POSIX")
formatter.dateFormat = "M/d/yy"
item1 = dictionary1.reduce(into: [:], { result, kv in
guard let date = formatter.date(from: kv.key) else { return }
result[date] = kv.value
})
item2 = dictionary2.reduce(into: [:], { result, kv in
guard let date = formatter.date(from: kv.key) else { return }
result[date] = kv.value
})
}
}
I want to convert String value from API to Custom Codable Model SWIFT
Nothing really complicated about that, the item has to use a secondary JsonDecoder
:
struct Item: Decodable {
let body: Body
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let jsonBody = try container.decode(String.self, forKey: .jsonBody)
let jsonBodyData = jsonBody.data(using: .utf8)!
let decoder = JSONDecoder()
body = try decoder.decode(Body.self, from: jsonBodyData)
}
private enum CodingKeys: String, CodingKey {
case jsonBody
}
}
struct Body: Decodable {
let documentInfo: ...
}
While using DateFormatter in swift error occurs
check your input Date format is wrong
let dateFormatter: DateFormatter = DateFormatter()
//2019-08-20T08:05:15.680Z.
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
if let dateFromTimeStamp: Date = dateFormatter.date(from: "2019-08-20T08:05:15.680Z"){
dateFormatter.dateFormat = "dd MMM yyyy, hh:mm a"
if let dateString = dateFormatter.string(from: dateFromTimeStamp) as? String{
print ("dateString == \(dateString)")
}
}
Related Topics
Inter-App Data Migration (Migrating Data to New App Version)
Remove or Edit User Location Blue Pulsing Circle
Making Buttons Appear and Disappear When Clicked
How to Fix Cocoapod .Modulemap File Not Found
How to Find String B Missing Characters Based on String a and Add Them to String B
How to Update the Constant Height Constraint of a Uiview Programmatically
Why Is an Observedobject Array Not Updated in My Swiftui Application
Go to a New View Using Swiftui
Rounding a Double Value to X Number of Decimal Places in Swift
Swift Generic Coercion Misunderstanding
Uncaught Error/Exception Handling in Swift
Using Decodable in Swift 4 With Inheritance
Differences in Nsdatecomponents Syntax
How to Compare Two Dictionaries in Swift