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
}()
}
Related
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")
}
}
}
}
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"
I am getting a response from an API but the problem is that the API is sending values back as a string of dates and doubles. I am therefore getting the error "Expected to decode Double but found a string/data instead." I have structured my struct like this to solve the problem but this seems like a patch. Is there any better way to fix this issue? I feel like apple has thought of this and included something natively to address this.
struct SimpleOrder:Codable{
var createdDate:Date! {
return createdDateString.dateFromISO8601
}
var createdDateString:String
var orderId:String!
var priceVal:Double!
var priceString:String{
didSet {
priceVal = Double(self.priceString)!
}
}
private enum CodingKeys: String, CodingKey {
//case createdDate
case createdDateString = "time"
case orderId = "id"
case priceVal
case priceString = "price"
}
}
I don't know if this is relevant but this is how it is being used. I am getting the data as a string and converting it to data which is stored in the dataFromString variable
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601 //This is irrelevant though because the date is returned as a string.
do{
let beer = try decoder.decode(SimpleOrder.self, from: dataFromString)
print ("beer is \(beer)")
}catch let error{
print ("error is \(error)")
}
As a result of using codable, I am getting an error when trying to get an empty instance of SimpleOrder. Before I was using codable, I had no issues using SimpleOrder() without any arguments.
Error: Cannot invoke initializer for type 'SimpleOrder' with no arguments
var singlePoint = SimpleOrder()
struct SimpleOrder: Codable {
var created: Date?
var orderId: String?
var price: String?
private enum CodingKeys: String, CodingKey {
case created = "time", orderId = "id", price
}
init(created: Date? = nil, orderId: String? = nil, price: String? = nil) {
self.created = created
self.orderId = orderId
self.price = price
}
}
extension SimpleOrder {
var priceValue: Double? {
guard let price = price else { return nil }
return Double(price)
}
}
extension 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:ss.SSSXXXXX"
return formatter
}()
}
Decoding the json data returned by the API:
let jsonData = Data("""
{
"time": "2017-12-01T20:41:48.700Z",
"id": "0001",
"price": "9.99"
}
""".utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(Formatter.iso8601)
do {
let simpleOrder = try decoder.decode(SimpleOrder.self, from: jsonData)
print(simpleOrder)
} catch {
print(error)
}
Or initialising a new object with no values:
var order = SimpleOrder()
So I've run into a tiny obstacle and I'm trying to access a variable created inside an Alamofire request function. A bit of background into:
Used SwiftyJSON/Alamofire to access JSON file and parse it, have a variable for a accessing date, but date was in RFC 3339 format and now I created a function to parse the date from RFC 339 to a readable format but i don't now how to access the date variable created in the JSON parse function to use with the Date parse function.
//Get the JSON from server
func getJSON() {
Alamofire.request(.GET, "link goes here").responseJSON { (Response) in
if let value = Response.result.value {
let json = JSON(value)
for anItem in json.array! {
let title: String? = anItem["Title"].stringValue
let date: String? = anItem["Date"].stringValue //trying to access this variable outside the function
let body: String? = anItem["Body"].stringValue
self.tableTitle.append(title!)
self.tableDate.append(date!)
self.tableBody.append(body!)
print(anItem["Title"].stringValue)
print(anItem["Date"].stringValue)
}
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
}
// this date stuff isn't being used yet, because I have no idea how...
public func dateForRFC3339DateTimeString(rfc3339DateTimeString: String) -> NSDate? {
let enUSPOSIXLocale = NSLocale(localeIdentifier: "en_US_POSIX")
let rfc3339DateFormatter = NSDateFormatter()
rfc3339DateFormatter.locale = enUSPOSIXLocale
rfc3339DateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
rfc3339DateFormatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
return rfc3339DateFormatter.dateFromString(rfc3339DateTimeString)
}
public func userVisibleDateTimeStringForRFC3339DateTimeString(rfc3339DateTimeString: String) -> String? {
let maybeDate = dateForRFC3339DateTimeString(rfc3339DateTimeString)
if let date = maybeDate {
let userVisibleDateFromatter = NSDateFormatter()
userVisibleDateFromatter.dateStyle = NSDateFormatterStyle.MediumStyle
userVisibleDateFromatter.timeStyle = NSDateFormatterStyle.ShortStyle
return userVisibleDateFromatter.stringFromDate(date)
} else {
return nil
}
}
let finalDateStr = userVisibleDateTimeStringForRFC3339DateTimeString(MasterViewController) //now this is where it gets weird, instead of letting me enter the string in the brackets, it defaults to MasterViewController, now I tried to move the date functions to another .swift file (an empty one) and it doesn't do that anymore
So yeah, that's about it, if anyone could help, it would be greatly appreciated.
Try below code, let me know if it works:
func dateForRFC3339Date(rfc3339Date: NSDate) -> NSDate? {
let enUSPOSIXLocale = NSLocale(localeIdentifier: "en_US_POSIX")
let rfc3339DateFormatter = NSDateFormatter()
rfc3339DateFormatter.locale = enUSPOSIXLocale
rfc3339DateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
rfc3339DateFormatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
let dateString = rfc3339DateFormatter.stringFromDate(rfc3339Date)
return rfc3339DateFormatter.dateFromString(dateString)
}
And call it in your getJSON() method like:
let convertedDate = self.dateForRFC3339Date(rfc3339Date: anItem["Date"])
I'm relatively new to iOS programming. However, I would have assumed that Swift would have an automated way of converting objects to JSON and vice versa. That being said, I have found several libraries that can do this.
HOWEVER...
It seems that no matter how you post data to a web service (even using something like AlamoFire), the requests must be a dictionary. All these forums show examples of how easy it is to convert the returned JSON string to objects. True. But the request needs to be manually coded. That is, go through all of the object properties and map them as a dictionary.
So my question is this: Am I missing something? Have I got this all wrong and there's a super-easy way to either (a) send JSON (instead of a dictionary) in the REQUEST or (b) convert an object automatically to a dictionary?
Again, I see how easy it is to deal with a JSON response. I'm just looking for an automatic way to convert the request object I want to post to a web service into a format that a library like AlamoFire (or whatever) requires. With other languages this is fairly trivial, so I'm hoping there's an equally easy and automated way with Swift.
I must disagree with #Darko.
In Swift 2,
use protocol oriented programming and the simple reflection offered by Mirror class :
protocol JSONAble {}
extension JSONAble {
func toDict() -> [String:Any] {
var dict = [String:Any]()
let otherSelf = Mirror(reflecting: self)
for child in otherSelf.children {
if let key = child.label {
dict[key] = child.value
}
}
return dict
}
}
then you can use this protocol with your request class and produce the desired dictionary :
class JsonRequest : JSONAble {
var param1 : String?
// ...
}
let request = JsonRequest()
// set params of the request
let dict = request.toDict()
// use your dict
My solution to this will be something like this:
extension Encodable {
var dict : [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] else { return nil }
return json
}
}
and usage will be something like this:
movies.compactMap { $0.dict }
Swift currently does not support advanced reflection like Java or C# so the answer is: no, there is not an equally easy and automated way with pure Swift.
[Update] Swift 4 has meanwhile the Codable protocol which allows serializing to/from JSON and PLIST.
typealias Codable = Decodable & Encodable
Without using reflection, and works for nested objects (Swift 4):
protocol Serializable {
var properties:Array<String> { get }
func valueForKey(key: String) -> Any?
func toDictionary() -> [String:Any]
}
extension Serializable {
func toDictionary() -> [String:Any] {
var dict:[String:Any] = [:]
for prop in self.properties {
if let val = self.valueForKey(key: prop) as? String {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Int {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Double {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Array<String> {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Serializable {
dict[prop] = val.toDictionary()
} else if let val = self.valueForKey(key: prop) as? Array<Serializable> {
var arr = Array<[String:Any]>()
for item in (val as Array<Serializable>) {
arr.append(item.toDictionary())
}
dict[prop] = arr
}
}
return dict
}
}
Just implement properties and valueForKey for the custom objects you want to convert. For example:
class Question {
let title:String
let answer:Int
init(title:String, answer:Int) {
self.title = title
self.answer = answer
}
}
extension Question : Serializable {
var properties: Array<String> {
return ["title", "answer"]
}
func valueForKey(key: String) -> Any? {
switch key {
case "title":
return title
case "answer":
return answer
default:
return nil
}
}
}
You can add more value types in the toDictionary function if you need.
The latest solution that I found after lots of digging throughout Stack Overflow is:
//This block of code used to convert object models to json string
let jsonData = try JSONEncoder().encode(requestData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
//This method is used to convert jsonstring to dictionary [String:Any]
func jsonToDictionary(from text: String) -> [String: Any]? {
guard let data = text.data(using: .utf8) else { return nil }
let anyResult = try? JSONSerialization.jsonObject(with: data, options: [])
return anyResult as? [String: Any]
}
//Use above method something like this
let params = jsonToDictionary(from: jsonString) ?? [String : Any]()
//Use params to pass in paramters
Alamofire.request(completeUrl, method: .post, parameters: params, encoding:JSONEncoding.prettyPrinted, headers: myHeaders){
response in
//Do whatever you want with response of it.
}
Note:
I combine this solution from multiple answers.
This solution i used with alamofire because alamofire only accept parameter at this format "[String:Any]".
You can also use the ObjectMapper library. It has a "toJSON" method that converts your object to a dictionary.
in short
let dict = Mirror(reflecting: self).children.map({ $0 }).reduce(into: [:]) { $0[$1.label] = $1.value }
Example how to use Mirror with conversion to specific Dictionary type:
protocol DictionaryConvertible { }
extension DictionaryConvertible {
func toDictionary() -> [String: CustomStringConvertible] {
Dictionary(
uniqueKeysWithValues: Mirror(reflecting: self).children
.compactMap { child in
if let label = child.label,
let value = child.value as? CustomStringConvertible {
return (label, value)
} else {
return nil
}
}
)
}
}