I have GeoJSON data from an API and it contains dates that are in ISO8601 format. I can decode them in SwiftUI as a string and manipulate them through a calculated field to get a version that is type Date but it's clumsy.
I know the JSONDecoder supports date en/decoding options and I'd like the same behaviour.. similar to this:
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
I was thinking of maybe an extension to MKGeoJSONDecoder but I can't figure out how to even get started because of the need to be in the parsing flow.
Thoughts? Thanks
Thanks vadian, all of the data I wanted was in the properties attribute so there was really no value in using MKGeoJSONDecoder. I just switched to normal JSONDecoder and put a custom ISO8601 formatter to match my data and all works great.
In case anyone wants to reference, here is the playground code. You'd want to add a bunch of error checking of course. And thanks to Sarunw for the great info on the custom date decoding (https://sarunw.com/posts/how-to-parse-iso8601-date-in-swift/)
import Foundation
import MapKit
struct Response: Codable {
let features: [Feature]
}
struct Feature: Codable {
let properties: Properties
}
struct Properties: Codable {
let stnNamValue: String?
let dateTmValue: Date?
enum CodingKeys: String, CodingKey {
case stnNamValue = "stn_nam-value"
case dateTmValue = "date_tm-value"
}
}
Task {
// get the data from API
let url = URL(string: "https://api.weather.gc.ca/collections/swob-realtime/items?f=json&sortby=-date_tm-value&clim_id-value=5050919&limit=3")
let (data, _) = try await URLSession.shared.data(from: url!)
// create the decoder with custom ISO8601 formatter
let decoder = JSONDecoder()
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate, .withFullTime,.withFractionalSeconds, .withTimeZone]
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
if let date = formatter.date(from: dateString) {return date}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
})
// decode & print the results
let decodedResponse = try decoder.decode(Response.self, from: data)
decodedResponse.features.forEach {(feature) in
print("Station: \(feature.properties.stnNamValue ?? "<no value>"), Date: \(feature.properties.dateTmValue)")
}
}
Output is the date/times of the most recent surface weather observations for Flin Flon, Manitoba:
Station: FLIN FLON, Date: Optional(2022-02-16 12:22:00 +0000)
Station: FLIN FLON, Date: Optional(2022-02-16 12:21:00 +0000)
Station: FLIN FLON, Date: Optional(2022-02-16 12:20:00 +0000)
Related
In my Vapor 3 app, I have an Event model, which has the properties startDate: Date and endDate: Date.
Now, I'm wondering how to pass those date values in a POST request. In Postman, I tried the following in x-www-form-urlencoded:
startDate -> 2019-03-14
This returns the error below:
Could not convert to Double: str(\"2019-03-14\")
Apparently, Date turns into Double.
So, instead, what value do I need to pass?
Note
I know, that, in Postman, I can insert {{$timestamp}}, but 1) this doesn't answer my question when using the API outside Postman and 2) this doesn't allow me to enter a date other than now.
So the issue here is that by default, a Date instance is decoded using the time interval since Jan 1, 2001. The URL form decoder that Vapor uses doesn't support different date strategies like the JSONDecoder does at the moment, so you'll have to do the decoding a different way. Here are a couple of ideas I could come up with:
Just send the timestamp in the request. For testing different dates in Postman, you can set an environment variable in the pre-request script and access that in the request body.
Manually implement the Event.init(from:) and .encode(to:) methods. Just to make sure you don't break the Fluent coding, you will probably have to add some extra logic, but it should work. Here's an example:
final class Event: Model {
static let formDateFormatter: 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"
return formatter
}()
var startDate: Date
var endDate: Date
init(from decoder: Decoder)throws {
let container = try decoder.container(keyedby: CodingKeys.self)
if let start = try? container.decode(String.self, keyedBy: .startDate), let date = Event.formDateFormatter.string(from: start) {
self.startDate = date
} else {
self.startDate = try container.decode(Date.self, keyedBy: .startDate)
}
if let end = try? container.decode(String.self, keyedBy: .endDate), let date = Event.formDateFormatter.string(from: end) {
self.endDate = date
} else {
self.endDate = try container.decode(Date.self, keyedBy: .endDate)
}
}
}
I'm not sure about x-www-form-urlencoded cause I tested it and if I send a date as 0 it decodes it as 2001-01-01 00:00:00 +0000 thought it definitely should be 1970-01-01 00:00:00 +0000.
But with JSON payload you have a flexibility cause you could provide a JSONDecoder configured as needed for you.
struct Payload: Content {
var date: Date
}
If you'd like to send dates as UNIX-timestamp
router.post("check") { req throws -> Future<String> in
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970 // choose it for unix-timestamp
return try req.content.decode(json: Payload.self, using: decoder).map { p in
return String(describing: p.date)
}
}
If you'd like to send dates in your own format
router.post("check") { req throws -> Future<String> in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter) // custom date formatter
return try req.content.decode(json: Payload.self, using: decoder).map { p in
return String(describing: p.date)
}
}
So for unix-timestamp you should send seconds from 1970 and e.g. 0 will be decoded to 1970-01-01 00:00:00 +0000.
And for custom format described above you should send dates like 2018-01-01 00:00:00 to decode it as 2018-01-01 00:00:00 +0000
UPD: you could write an extension to decode it beautifully
extension ContentContainer where M: Request {
func decodeJson<D>(_ payload: D.Type) throws -> Future<D> where D: Decodable {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
return try decode(json: payload, using: decoder)
}
}
so then you'll be able to decode your payload like this
router.post("check") { (req) throws -> Future<String> in
return try req.content.decodeJson(Payload.self).map { p in
return String(describing: p.date)
}
}
I realized that the Date object is returned in the following format when queried:
2021-12-31T14:29:00Z
So this is what I tried to pass and that worked! No need for any custom decoding.
We used Decodable, and have a time: Date field, but from server came only time with "HH:mm:ss" format. Another date parsers with DateInRegion.
But this field crash app
I try do smth in decoder, but cant see any properties (.count)
and I dont now what need do
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ decoder -> Date in
do {
let value = try decoder.singleValueContainer()
let string = try value.decode(String.self)
if string.conut == 8 {
if let date = DateInRegion(
string: string,
format: .iso8601(options: .withTime),
fromRegion: Region.Local()
) {
return date.absoluteDate
} else {
throw DecodingError.nil
}
} else if let date = DateInRegion(
string: string,
format: .iso8601(options: .withInternetDateTime),
fromRegion: Region.Local()
) {
return date.absoluteDate
} else {
throw DecodingError.nil
}
} catch let error {
Not sure what you are trying to achieve, but it seems you want to handle multiple date format in a single JSON decoder.
Given the response struct is Decodable:
struct Response: Decodable {
var time: Date
var time2: Date
}
You can configure your decoder to handle all the date format you want:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
// Extract string date from the container
let container = try decoder.singleValueContainer()
let stringDate = try container.decode(String.self)
// Trying to parse the "HH:mm:ss" format
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
if let date = dateFormatter.date(from: stringDate) {
return date // successfully parsed "HH:mm:ss" format
}
// Trying to parse the iso8601 format
if let date = ISO8601DateFormatter().date(from: stringDate) {
return date // successfully parsed iso8601 format.
}
// Unknown date format, throw an error.
let context = DecodingError.Context(codingPath: [], debugDescription: "Unable to parse a date from '\(stringDate)'")
throw DecodingError.dataCorrupted(context)
})
Note: This is an example code and it's not very optimized (DateFormatter init each time you want to parse a date, using the brute force parsing to determine whether the date is in "HH:mm:ss" format or not, ...).
I think, the best way to optimize this, is to use multiple decoders, each configured for a specific date format, and use the right one when needed. For example, if you are trying to parse a JSON from an API, you should be able to determine whether you need to parse an iso8601 format or something else, and use the right decoder accordingly.
Here is an example of the decoder working in a playground:
let json = """
{"time": "11:05:45", "time2": "2018-09-28T16:02:55+00:00"}
"""
let response = try! decoder.decode(Response.self, from: json.data(using: .utf8)!)
response.time // Jan 1, 2000 at 11:05 AM
response.time2 // Sep 28, 2018 at 6:02 PM
I am parsing json from server. I am getting 4 values in json so I created model class
class PriceData: Mappable {
var buy: Double?
var sell: Double?
var spot_price: NSNumber?
var timestamp: String?
var timesStampDt: Date?
required init?(map: Map) {
//
}
func mapping(map: Map) {
buy <- map["buy"]
sell <- map["sell"]
spot_price <- map["spot_price"]
timestamp <- map["timestamp"]
print(String(describing: GlobalMethods.dateFormat(dt: timestamp!)))
timesStampDt <- map[String(describing: GlobalMethods.dateFormat(dt:
timestamp!))]
}
}
I am geting timestamp as string type but I need to covert into date while parsing so I converted timeStamp to date by using this method and
static func dateFormat(dt: String) -> Date{
let formatter = Foundation.DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let date1 = formatter.date(from: dt)
print("date:\(String(describing: date1))")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let resultTime = formatter.date(from: dt)
return resultTime!
}
but I when passing the converted date to "timesStampDt" in mapping func
the value of timesStampDt is nil.
You probably want to use Transform method from object mapper. Check DateTransform class and use it like follows.
timesStampDt <- (map["timestamp"], DateTransform())
Tis will transform your timestamp to Date. You can further study the transform class and make any Object Transform for yourself, This is very flexible
Why don’t you simply let JSONDecoder handle the date parsing? You can set your format on the decoder to configure in what format you get your date with so the decoder knows how to decode it.
Doing it with JSONDecoder and Codable makes parse code a lot cleaner.
Here is a Codable tutorial for you to read.
When you call .decode() to decode a struct, what exactly does it return?
I have look it up on the Apple Documentation, but all it says is "native format into in-memory representations." But what does this mean? Can anyone help me?
I'm asking this because my app is crashing when I get a null value from the JSON data, from this line of code:
let plantData = try decoder.decode([Plants].self, from: data)
Here is my struct:
struct Plants: Codable {
let date: String
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And Here is my Parsing code:
func parseJson() {
let url = URL(string: ebr_String)
// Load the URL
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
// If there are any errors don't try to parse it, show the error
guard let data = data, error == nil else { print(error!); return }
let decoder = JSONDecoder()
do{
let plantData = try decoder.decode([Plants].self, from: data)
print(plantData)
And Here is just a snippet of the information I am getting back:
MorrowTabbedApp.Plants(date: "2018-02-22", monthlyAVG: Optional("1210.06")), MorrowTabbedApp.Plants(date: "2018-02-23", monthlyAVG: nil)]
Here is the snippet of JSON from the web:
[
{"Date":"2018-02-21","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-22","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-23","30_Day_MA_MMBTU":null}
]
The decode method of JSONDecoder is a "generic" method. It returns an instance of whatever type you specified in the first parameter of the method. In your case, it returns a [Plants], i.e. a Array<Plants>, i.e. a Swift array of Plants instances.
If it's crashing because of a null value in your JSON, then you have to identify what was null, whether it was appropriate to be null, and if so, make sure that any Plants properties associated with values that might be null should be optionals.
Given your updated answer with code snippets, I'd suggest:
// Personally, I'd call this `Plant` as it appears to represent a single instance
struct Plant: Codable {
let date: String
let monthlyAVG: String? // Or you can use `String!` if you don't want to manually unwrap this every time you use it
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And:
do {
let plantData = try JSONDecoder().decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
Note the filter line which selects only those occurrences for which monthlyAVG is not nil.
A couple of other suggestions:
Personally, if you could, I'd rather see the web service designed to only return the values you want (those with an actual monthlyAVG) and then change the monthlyAVG property to not be an optional. But that's up to you.
If monthlyAVG is really a numeric average, I'd change the web service to not return it as a string at all, but as a number without quotes. And then change the property of Plant to be Double or whatever.
You could, if you wanted, change the date property to be a Date and then use dateDecodingStrategy to convert the string to a Date:
struct Plant: Codable {
let date: Date
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
and
do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
let plantData = try decoder.decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
You might do this if, for example, you wanted the x-axis of your chart to actually represent time rather than an evenly spaced set of data points.
This question already has answers here:
How to convert a date string with optional fractional seconds using Codable in Swift?
(4 answers)
Closed 5 years ago.
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
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
})
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.