Decoding Date values with different JSON formats in Swift? [duplicate] - json

This question already has answers here:
How to convert a date string with optional fractional seconds using Codable in Swift?
(4 answers)
Closed 3 years ago.
Suppose we have the following structure:
struct TestCod: Codable {
var txt = ""
var date = Date()
}
If we are expecting JSON in two similar formats, how could we handle decoding?
JSON A:
{
"txt":"stack",
"date":589331953.61679399
}
JSON B:
{
"txt":"overflow",
"date":"2019-09-05"
}

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"

Related

Decode date/time from String or TimeInterval in Swift

I have a JSON (from a third party) that I need to parse. This JSON returns several nested objects
articles: {
authors: {
birthday: 'DD-MM-YYYY'
}
relevant_until: 'YYYY-MM-DD HH:MM:SS'
publication_date: secondsSince1970,
last_comment: iso8601
}
I'm following this answer to have multiple date formatters and it works, as long as every date extracted from JSON is a string.
But when it comes to the secondsSince1970 (UNIX epoc time) I can't find a way to parse it as a codable object. Everywhere I see the Date(timeIntervalSince1970: timestamp) and I don't know how to use it when decoding it
How do I parse the dates on this object when a date can be passed as a TimeInterval or as a String?
try jsonDecoder.decode(Articles.self, from: jsonData)
extension Formatter {
static let iso8601withFractionalSeconds: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
static let ddMMyyyy: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "dd-MM-yyyy"
return formatter
}()
}
extension JSONDecoder.DateDecodingStrategy {
static let multiple = custom {
let container = try $0.singleValueContainer()
do {
return try Date(timeIntervalSince1970: container.decode(Double.self))
} catch DecodingError.typeMismatch {
let string = try container.decode(String.self)
if let date = Formatter.iso8601withFractionalSeconds.date(from: string) ??
Formatter.iso8601.date(from: string) ??
Formatter.ddMMyyyy.date(from: string) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
}
}
Playground testing:
struct Root: Codable {
let articles: Articles
}
struct Articles: Codable {
let authors: Authors
let relevantUntil: Date
let publicationDate: Date
let lastComment: Date
}
struct Authors: Codable {
let birthday: Date
}
let json = """
{"articles": {
"authors": {"birthday": "01-01-1970"},
"relevant_until": "2020-11-19 01:23:45",
"publication_date": 1605705003.0019,
"last_comment": "2020-11-19 01:23:45.678"}
}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .multiple
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let root = try decoder.decode(Root.self, from: .init(json.utf8))
print(root.articles) // Articles(authors: __lldb_expr_107.Authors(birthday: 1970-01-01 03:00:00 +0000), relevantUntil: 2020-11-19 04:23:45 +0000, publicationDate: 2020-11-18 13:10:03 +0000, lastComment: 2020-11-19 04:23:45 +0000)
} catch {
print(error)
}
Following the same logic you can try to decode the JSON property as TimeInterval (or Double) and if that fails, fall back to your String handling:
extension JSONDecoder {
var dateDecodingStrategyFormatters: [DateFormatter]? {
#available(*, unavailable, message: "This variable is meant to be set only")
get { return nil }
set {
guard let formatters = newValue else { return }
self.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
do {
let timeInterval = try container.decode(TimeInterval.self)
return Date(timeIntervalSince1970: timeInterval)
} catch DecodingError.typeMismatch {
let dateString = try container.decode(String.self)
for formatter in formatters {
if let date = formatter.date(from: dateString) {
return date
}
}
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date")
}
}
}
}

Swift ISO 8601 date formatting

I'm making an api call to randomuser.me and getting back a json file containing (amongst other data):
"dob": {
"date": "1993-07-20T09:44:18.674Z",
"age": 26
}
I'd like to display the date string in a text label as "dd-MMM-yyyy" but can't figure out how to format the string to achieve this.
I'v tried converting it into a date using ISO8601DateFormatter and then back into a string but have had no luck so far.
Can anyone help?
func getUserData() {
let config = URLSessionConfiguration.default
config.urlCache = URLCache.shared
let session = URLSession(configuration: config)
let url = URL(string: "https://randomuser.me/api/?page=\(page)&results=20&seed=abc")!
let urlRequest = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 15.0)
let task = session.dataTask(with: urlRequest) { data, response, error in
// Check for errors
guard error == nil else {
print ("error: \(error!)")
return
}
// Check that data has been returned
guard let content = data else {
print("No data")
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let fetchedData = try decoder.decode(User.self, from: content)
for entry in fetchedData.results {
self.usersData.append(entry)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let err {
print("Err", err)
}
}
// Execute the HTTP request
task.resume()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "userInfoCell") as! UserInfoCell
let user: Result
if isFiltering {
user = filteredData[indexPath.row]
} else {
user = usersData[indexPath.row]
}
cell.nameLabel.text = "\(user.name.first) \(user.name.last)"
cell.dateOfBirthLabel.text = user.dob.date
cell.genderLabel.text = user.gender.rawValue
cell.thumbnailImage.loadImageFromURL(user.picture.thumbnail)
return cell
}
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.
Generally, if manually converting 1993-07-20T09:44:18.674Z to a Date, we’d use ISO8601DateFormatter:
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds)
In this approach, it takes care of time zones and locales for us.
That having been said, if you’re using JSONDecoder (and its dateDecodingStrategy outlined below), then we should define our model objects to use Date types, not String for all the dates. Then we tell the JSONDecoder to decode our Date types for us, using a particular dateDecodingStrategy.
But that can’t use the ISO8601DateFormatter. We have to use a DateFormatter with a dateFormat of "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" and a locale of Locale(identifier: "en_US_POSIX"):
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0) // this line only needed if you ever use the same formatter to convert `Date` objects back to strings, e.g. in `dateEncodingStrategy` of `JSONEncoder`
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(formatter)
See the “Working With Fixed Format Date Representations” sections of the DateFormatter documentation.
Then, for your formatter for your UI, if you absolutely want dd-MMM-yyyy format, you would have a separate formatter for that, e.g.:
let formatter = DateFormatter()
formatter.dateFormat = "dd-MMM-yyyy"
Note, for this UI date formatter, we don’t set a locale or a timeZone, but rather let it use the device’s current defaults.
That having been said, we generally like to avoid using dateFormat for date strings presented in the UI. We generally prefer dateStyle, which shows the date in a format preferred by the user:
let formatter = DateFormatter()
formatter.dateStyle = .medium
This way, the US user will see “Nov 22, 2019”, the UK user will see “22 Nov 2019”, and the French user will see “22 nov. 2019”. Users will see dates in the formats with which they are most accustomed.
See “Working With User-Visible Representations of Dates and Times” in the aforementioned DateFormatter documentation.
pass your date into this
let dateFormatterPrint = DateFormatter()
dateFormatterPrint.dateFormat = "dd-MM-yyyy"
let val = dateFormatterPrint.string(from: "pass your date")
You can use extensions for you to be able to convert the date to your desired format.
var todayDate = "1993-07-20T09:44:18.674Z"
extension String {
func convertDate(currentFormat: String, toFormat : String) -> String {
let dateFormator = DateFormatter()
dateFormator.dateFormat = currentFormat
let resultDate = dateFormator.date(from: self)
dateFormator.dateFormat = toFormat
return dateFormator.string(from: resultDate!)
}
}
Then you can implement like this:
cell.dateOfBirthLabel.text = self.todayDate.convertDate(currentFormat: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", toFormat: "dd-MMM-yyyy")

How to JSON encode multiple date formats within the same struct

Need to encode into JSON, a struct that has 2 Date instance variables (day and time), however, I need to encode each date instance variable with a different format, ie. for "day":"yyyy-M-d" and "time":"H:m:s".
Have written a custom decoder which works no problems. But not sure how to write the required custom encoder to solve this.
For example I can decode the following JSON string:
{ "biometrics" : [
{"biometricId":1,"amount":2.1,"source":"Alderaan","day":"2019-1-3","time":"11-3-3","unitId":2},
{"biometricId":10,"amount":3.1,"source":"Endoor","day":"2019-2-4","time":"11-4-4","unitId":20}]
}
However, when I encode it, I can only encode it in a single date format :(
Help, would be greatly appreciated.
Thank you.
import UIKit
let biometricsJson = """
{ "biometrics" : [
{"biometricId":1,"amount":2.1,"source":"Alderaan","day":"2019-1-3","time":"11-3-3","unitId":2},
{"biometricId":10,"amount":3.1,"source":"Endoor","day":"2019-2-4","time":"11-4-4","unitId":20}]
}
"""
struct Biometrics: Codable {
var biometrics: [Biometric]
}
struct Biometric: Codable {
var biometricId: Int
var unitId: Int
var source: String?
var amount: Double
var day: Date
var time: Date
init(biometricId: Int, unitId: Int, source: String, amount: Double, day: Date, time: Date){
self.biometricId = biometricId
self.unitId = unitId
self.source = source
self.amount = amount
self.day = day
self.time = time
}
}
extension Biometric {
static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "H:m:s"
if let date = formatter.date(from: dateString) {
return date
}
formatter.dateFormat = "yyyy-M-d"
if let date = formatter.date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Cannot decode date string \(dateString)")
}
return decoder
}()
}
let biometrics = try Biometric.decoder.decode(Biometrics.self, from:biometricsJson.data(using: .utf8)!)
let jsonEncoder = JSONEncoder()
let encodedJson = try jsonEncoder.encode(biometrics)
let jsonString = String(data: encodedJson, encoding: .utf8)
if biometricsJson != jsonString {
print("error: decoding, then encoding does not give the same string")
print("biometricsJson: \(biometricsJson)")
print("jsonString: \(jsonString!)")
}
I expect the encoded JSON, to be decodable by the decoder.
i.e. biometricsJson == jsonString
In a custom encode(to:), just encode each one as a string using the desired formatter. There's no "date" type in JSON; it's just a string. Something along these lines:
enum CodingKeys: CodingKey {
case biometricId, amount, source, day, time, unitId
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(biometricId, forKey: .biometricId)
try container.encode(unitId, forKey: .unitId)
try container.encode(source, forKey: .source)
try container.encode(amount, forKey: .amount)
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "H:m:s"
let timeString = formatter.string(from: time)
try container.encode(timeString, forKey: .time)
formatter.dateFormat = "yyyy-M-d"
let dayString = formatter.string(from: day)
try container.encode(dayString, forKey: .day)
}
But note that you can't test for equivalent strings. JSON dictionaries aren't order-preserving, so there's no way to guarantee a character-by-character match.
Note that if you really want to have days and times, you should consider DateComponents rather than a Date. A date is a specific instance in time; it's not in any time zone, and it can't be just an hour, minute, and second.
Also, your use of Double is going to cause rounding differences. So 2.1 will be encoded as 2.1000000000000001. If that's a problem, you should use Decimal for amount rather than Double.

How can I easily see the JSON output from my objects that conform to the `Codable` Protocol

I deal with lots of objects that I serialize/deserialize to JSON using the Codable protocol.
It isn't that hard to create a JSONEncoder, set it up to pretty-print, convert the object to JSON, and then convert that to a string, but seems like a lot of work. Is there a simple way to say "please show me the JSON output for this object?"
EDIT:
Say for example I have the following structures:
struct Foo: Codable {
let string1: String?
let string2: String?
let date: Date
let val: Int
let aBar: Bar
}
struct Bar: Codable {
let name: String
}
And say I've created a Foo object:
let aBar = Bar(name: "Fred")
let aFoo = Foo(string1: "string1", string2: "string2", date: Date(), val: 42, aBar: aBar)
I could print that with a half-dozen lines of custom code:
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let data = try? encoder.encode(aFoo),
let output = String(data: data, encoding: .utf8)
else { fatalError( "Error converting \(aFoo) to JSON string") }
print("JSON string = \(output)")
Which would give the output:
JSON string = {
"date" : 557547327.56354201,
"aBar" : {
"name" : "Fred"
},
"string1" : "string1",
"val" : 42,
"string2" : "string2"
}
I get tired of writing the same half-dozen lines of code each time I need it. Is there an easier way?
I would recommend creating a static encoder so you don't create a new encoder every time you call that property:
extension JSONEncoder {
static let shared = JSONEncoder()
static let iso8601 = JSONEncoder(dateEncodingStrategy: .iso8601)
static let iso8601PrettyPrinted = JSONEncoder(dateEncodingStrategy: .iso8601, outputFormatting: .prettyPrinted)
}
extension JSONEncoder {
convenience init(dateEncodingStrategy: DateEncodingStrategy,
outputFormatting: OutputFormatting = [],
keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys) {
self.init()
self.dateEncodingStrategy = dateEncodingStrategy
self.outputFormatting = outputFormatting
self.keyEncodingStrategy = keyEncodingStrategy
}
}
Considering that you are calling this method inside a Encodable extension you can just force try!. You can also force the conversion from data to string:
extension Encodable {
func data(using encoder: JSONEncoder = .iso8601) throws -> Data {
try encoder.encode(self)
}
func dataPrettyPrinted() throws -> Data {
try JSONEncoder.iso8601PrettyPrinted.encode(self)
}
// edit if you need the data using a custom date formatter
func dataDateFormatted(with dateFormatter: DateFormatter) throws -> Data {
JSONEncoder.shared.dateEncodingStrategy = .formatted(dateFormatter)
return try JSONEncoder.shared.encode(self)
}
func json() throws -> String {
String(data: try data(), encoding: .utf8) ?? ""
}
func jsonPrettyPrinted() throws -> String {
String(data: try dataPrettyPrinted(), encoding: .utf8) ?? ""
}
func jsonDateFormatted(with dateFormatter: DateFormatter) throws -> String {
return String(data: try dataDateFormatted(with: dateFormatter), encoding: .utf8) ?? ""
}
}
Playground testing
struct Foo: Codable {
let string1: String
let string2: String
let date: Date
let val: Int
let bar: Bar
}
struct Bar: Codable {
let name: String
}
let bar = Bar(name: "Fred")
let foo = Foo(string1: "string1", string2: "string2", date: Date(), val: 42, bar: bar)
try! print("JSON\n=================\n", foo.json(), terminator: "\n\n")
try! print("JSONPrettyPrinted\n=================\n", foo.jsonPrettyPrinted(), terminator: "\n\n")
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
try! print("JSONDateFormatted\n=================\n", foo.jsonDateFormatted(with: dateFormatter))
This will print
JSON
=================
{"date":"2020-11-06T20:22:55Z","bar":{"name":"Fred"},"string1":"string1","val":42,"string2":"string2"}
JSONPrettyPrinted
=================
{
"date" : "2020-11-06T20:22:55Z",
"bar" : {
"name" : "Fred"
},
"string1" : "string1",
"val" : 42,
"string2" : "string2"
}
JSONDateFormatted
=================
{"date":"6 November 2020","bar":{"name":"Fred"},"string1":"string1","val":42,"string2":"string2"}
There isn't a stock way to convert a Codable object graph to a "pretty" JSON string, but it's pretty easy to define a protocol to do it so you don't write the same conversion code over and over.
You can simply create an extension to the Encodable protocol, like this:
extension Encodable {
var prettyJSON: String {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let data = try? encoder.encode(self),
let output = String(data: data, encoding: .utf8)
else { return "Error converting \(self) to JSON string" }
return output
}
}
Then for any JSON object
print(myJSONobject.prettyJSON)
and it displays the JSON text in "pretty printed" form.
One thing the above won't do is support custom formatting of dates. To do that we can modify prettyJSON to be a function rather than a computed property, where it takes an optional DateFormatter as a parameter with a default value of nil.
extension Encodable {
func prettyJSON(formatter: DateFormatter? = nil) -> String {
let encoder = JSONEncoder()
if let formatter = formatter {
encoder.dateEncodingStrategy = .formatted(formatter)
}
encoder.outputFormatting = .prettyPrinted
guard let data = try? encoder.encode(self),
let output = String(data: data, encoding: .utf8)
else { return "Error converting \(self) to JSON string" }
return output
}
}
Then you can use it just like the above, except that you need to add parentheses after prettyJSON, e.g.
print(myJSONobject.prettyJSON())
That form ignores the new DateFormatter parameter, and will output the same JSON string as the above. However,If you have a custom date formatter:
var formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy HH:mm:ss"
print(myJSONobject.prettyJSON(formatter: formatter))
Then the dates in your object graph will be formatted using the specified DateFormatter

Use SwiftyJSON to deserialize NSDate

What is the best way to deserialize a JSON date into an NSDate instance using SwiftyJSON?
Is it to just use stringValue with NSDateFormatter or is there a built in date API method with SwiftyJSON?
It sounds like NSDate support isn't built in to SwiftyJSON, but you can extend the JSON class with your own convenience accessors.
The following code is adapted from this GitHub issue.
extension JSON {
public var date: NSDate? {
get {
if let str = self.string {
return JSON.jsonDateFormatter.dateFromString(str)
}
return nil
}
}
private static let jsonDateFormatter: NSDateFormatter = {
let fmt = NSDateFormatter()
fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
fmt.timeZone = NSTimeZone(forSecondsFromGMT: 0)
return fmt
}()
}
Example:
let j = JSON(data: "{\"abc\":\"2016-04-23T02:02:16.797Z\"}".dataUsingEncoding(NSUTF8StringEncoding)!)
print(j["abc"].date) // prints Optional(2016-04-23 02:02:16 +0000)
You might need to tweak the date formatter for your own data; see this question for more examples. Also see this question about date formatting in JSON.
Swift 3 Version
extension JSON {
public var date: Date? {
get {
if let str = self.string {
return JSON.jsonDateFormatter.date(from: str)
}
return nil
}
}
private static let jsonDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
dateFormatter.timeZone = TimeZone.autoupdatingCurrent
return dateFormatter
}()
}