Parsing arbitrary format JSON date with Swift Decodable - json

I am attempting to format a date in a JSON document into the format "mm-dd-yyyy". I have the following data:
{"data":[{
"id": 123,
"url": "https://www.google.com",
"title": "The Google link",
"created_at": "2017-08-29T04:00:00.000Z",//date to format
"sent": true,
"alternative": "https://google.com",
"things": [],
"description": [
"search",
"lookup"
],
"company": "Alphabet"
}]}
This is my struct:
struct Sitedata: Decodable{
let data: [site]
}
struct site: Decodable {
let id: Int
let url: String
let title: String
let created_at: String
let sent: Bool
let alternative: String
let things: [String]
let description: [String]
let company: String
}
let sites = try JSONDecoder().decode(Sitedata.self, from: data)
I tried the following method but it produced nil:
func date(dateString: String){
// var dateString = "14.01.2017T14:54:00"
let format = "dd.MM.yyyy'T'HH:mm:ss"
let date = Date()
print("original String with date: \(dateString)")
print("date String() to Date(): \(dateString.toDate(format: format)!)")
print("date String() to formated date String(): \(dateString.toDateString(inputFormat: format, outputFormat: "dd MMMM")!)")
print("format Date(): \(date.toString(format: "dd MMM HH:mm")!)")
}
extension DateFormatter {
convenience init (format: String) {
self.init()
dateFormat = format
locale = Locale.current
}
}
extension String {
func toDate (format: String) -> Date? {
return DateFormatter(format: format).date(from: self)
}
func toDateString (inputFormat: String, outputFormat:String) -> String? {
if let date = toDate(format: inputFormat) {
return DateFormatter(format: outputFormat).string(from: date)
}
return nil
}
}
extension Date {
func toString (format:String) -> String? {
return DateFormatter(format: format).string(from: self)
}
}
How would I be able to parse and then format this date to MM-dd-yyyy?

First, follow the Swift naming convention: UpperCamelCase for class name and lowerCamelCase for variable name. Second, do yourself a favor and make created_at a Date field, like it's clearly is. That will save you a ton of headache later on.
Here's the code:
struct SiteData: Decodable{
let data: [Site]
}
struct Site: Decodable {
let id: Int
let url: String
let title: String
let created_at: Date // changed to Date
let sent: Bool
let alternative: String
let things: [String]
let description: [String]
let company: String
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let sites = try decoder.decode(SiteData.self, from: json)
Now that created_at has been parsed from JSON as a proper Date, you can format it however you like:
let formatter2 = DateFormatter()
formatter2.dateFormat = "MM-dd-yyyy"
print(formatter2.string(from: sites.data[0].created_at))
Note that DateFormatter is quite expensive to create and changing its dateFormat property is even more so. If you have to format a lot of dates to strings (or vice versa), create the date formatters only once and keep reusing them.

Related

Trouble Decoding JSON Data with Swift

Trying to get a little practice in decoding JSON data, and I am having a problem. I know the URL is valid, but for some reason my decoder keeps throwing an error. Below is my model struct, the JSON object I'm trying to decode, and my decoder.
Model Struct:
struct Event: Identifiable, Decodable {
let id: Int
let description: String
let title: String
let timestamp: String
let image: String
let phone: String
let date: String
let locationline1: String
let locationline2: String
}
struct EventResponse: Decodable {
let request: [Event]
}
JSON Response:
[
{
"id": 1,
"description": "Rebel Forces spotted on Hoth. Quell their rebellion for the Empire.",
"title": "Stop Rebel Forces",
"timestamp": "2015-06-18T17:02:02.614Z",
"image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Battle_of_Hoth.jpg",
"date": "2015-06-18T23:30:00.000Z",
"locationline1": "Hoth",
"locationline2": "Anoat System"
},
{
"id": 2,
"description": "All force-sensitive members of the Empire must report to the Sith Academy on Korriban. Test your passion, attain power, to defeat your enemy on the way to becoming a Dark Lord of the Sith",
"title": "Sith Academy Orientation",
"timestamp": "2015-06-18T21:52:42.865Z",
"image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Korriban_Valley_TOR.jpg",
"phone": "1 (800) 545-5334",
"date": "2015-09-27T15:00:00.000Z",
"locationline1": "Korriban",
"locationline2": "Horuset System"
},
{
"id": 3,
"description": "There is trade dispute between the Trade Federation and the outlying systems of the Galactic Republic, which has led to a blockade of the small planet of Naboo. You must smuggle supplies and rations to citizens of Naboo through the blockade of Trade Federation Battleships",
"title": "Run the Naboo Blockade",
"timestamp": "2015-06-26T03:50:54.161Z",
"image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Blockade.jpg",
"phone": "1 (949) 172-0789",
"date": "2015-07-12T19:08:00.000Z",
"locationline1": "Naboo",
"locationline2": "Naboo System"
}
]
My Decoder:
func getEvents(completed: #escaping (Result<[Event], APError>) -> Void) {
guard let url = URL(string: eventURL) else {
completed(.failure(.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, response, error in
if let _ = error {
completed(.failure(.unableToComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completed(.failure(.invalidResponse))
return
}
guard let data = data else {
completed(.failure(.invalidData))
return
}
do {
let decoder = JSONDecoder()
let decodedResponse = try decoder.decode(EventResponse.self, from: data)
completed(.success(decodedResponse.request))
} catch {
completed(.failure(.invalidData))
}
}
task.resume()
}
I am sure the answer is pretty obvious to some but I have been beating my head against a wall. Thanks.
The EventResponse suggests that the JSON will be of the form:
{
"request": [...]
}
But that is obviously not what your JSON contains.
But you can replace:
let decodedResponse = try decoder.decode(EventResponse.self, from: data)
With:
let decodedResponse = try decoder.decode([Event].self, from: data)
And the EventResponse type is no longer needed.
FWIW, in the catch block, you are returning a .invalidData error. But the error that was thrown by decode(_:from:) includes meaning information about the parsing problem. I would suggest capturing/displaying that original error, as it will tell you exactly why it failed. Either print the error message in the catch block, or include the original error as an associated value in the invalidData error. But as it stands, you are discarding all of the useful information included in the error thrown by decode(_:from:).
Unrelated, but you might change Event to use URL and Date types:
struct Event: Identifiable, Decodable {
let id: Int
let description: String
let title: String
let timestamp: Date
let image: URL
let phone: String
let date: Date
let locationline1: String
let locationline2: String
}
And configure your date formatted to parse those dates for you:
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0) // not necessary because you have timezone in the date string, but useful if you ever use this formatter with `JSONEncoder`
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSX"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
And if you want to have your Swift code follow camelCase naming conventions, even when the API does not, you can manually specify your coding keys:
struct Event: Identifiable, Decodable {
let id: Int
let description: String
let title: String
let timestamp: Date
let image: URL
let phone: String
let date: Date
let locationLine1: String
let locationLine2: String
enum CodingKeys: String, CodingKey {
case id, description, title, timestamp, image, phone, date
case locationLine1 = "locationline1"
case locationLine2 = "locationline2"
}
}

Swift Parsing with Json

I'm trying to parsing Json using swift and this is the json I get
{"Data":{"KanBan":[{"Sdate":"2020/06/22","Stype":"上班卡","Stime":"09:31:11","Ip":"xxx.xxx.xxx.xx"},{"Sdate":"2020/06/20","Stype":"下班卡","Stime":"20:53:43","Ip":"xxx.xxx.xxx.xx"},{"Sdate":"2020/06/20","Stype":"下班卡","Stime":"20:48:25","Ip":"xxx.xxx.xxx.xx"},{"Sdate":"2020/06/20","Stype":"下班卡","Stime":"18:53:57","Ip":"xxx.xxx.xxx.xx"},{"Sdate":"2020/06/20","Stype":"上班卡","Stime":"18:43:54","Ip":"xxx.xxx.xxx.xx "}]},"IsSuccess":true,"Message":null,"ErrorCode":null,"ErrorMessage":null}
Below is the struct I defined and use it with Swift JsonDecoder :
// inside viewController
let decoder = JSONDecoder()
let historys = try decoder.decode([Datas].self, from: data)
// struct I defined myself
struct Datas : Codable {
let Data: KanBan
let IsSuccess: Bool
let Message: String
let ErrorCode: String
let ErrorMessage: String
init( Data: KanBan, IsSuccess: Bool, Message: String, ErrorCode: String, ErrorMessage: String) {
self.Data = Data
self.IsSuccess = IsSuccess
self.Message = Message
self.ErrorCode = ErrorCode
self.ErrorMessage = ErrorMessage
}
}
struct KanBan : Codable {
let KanBan: [details]
init( kanbans : [details]) {
self.KanBan = kanbans
}
}
struct details: Codable {
let Sdate: Date
let Stype: String
let Stime : String
let Ip: String
init(Sdate: Date, Stype: String, Stime : String, Ip: String ) {
self.Sdate = Sdate
self.Stype = Stype
self.Stime = Stime
self.Ip = Ip
}
}
I keep get error that said parsing failed .
Whats wrong with the struct I defined ? Plz help and thanks!!!
You need to fix 3 things:
You're trying to decode [Datas] whereas it should be Datas.
If the keys are optional you should declare them as optional. eg: ErrorMessage, ErrorCode.
Provide the DateFormatter if you're trying to decode Dates.
Here's the code you need: -
Model:
struct Datas: Codable {
let Data: KanBan
let IsSuccess: Bool
let Message: String?
let ErrorCode: String?
let ErrorMessage: String?
}
struct KanBan: Codable {
let KanBan: [details]
}
struct details: Codable {
let Sdate: Date
let Stype: String
let Stime : String
let Ip: String
}
Decoding:
do {
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let historys = try decoder.decode(Datas.self, from: data)
print(historys)
} catch { print(error) }

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

SWIFT 4, Xcode 9, JSON DECODER

I'm pretty stuck now. I'm attempting to PARS a JSON RETURN for just the year make make and model. It's buried in an array of dictionaries, and the decoder is having a hard time pulling them out. What am I doing wrong?
public struct Page: Decodable {
let Count: Int
let Message: String
let SearchCriteria: String
let Results: [car]}
public struct car: Decodable {
let ModelYear: String
let Make: String
let Model: String
let VIN: String}
let session = URLSession.shared
let components = NSURLComponents()
components.scheme = "https"
components.host = "vpic.nhtsa.dot.gov"
components.path = "/api/vehicles/decodevinvaluesextended/\(VIN)"
components.queryItems = [NSURLQueryItem]() as [URLQueryItem]
let queryItem1 = NSURLQueryItem(name: "Format", value: "json")
components.queryItems!.append(queryItem1 as URLQueryItem)
print(components.url!)
let task = session.dataTask(with: components.url!, completionHandler: {(data, response, error) in
guard let data = data else { return }
do
{
//let Result = try JSONDecoder().decode(Page.self, from: data)
// let PageResult = try JSONDecoder().decode(Page.self, from: data)
let json = try JSONDecoder().decode(Page.self, from: data)
let Results = json.Results;
print(Results)
First of all it's highly recommended to conform to the Swift naming convention that variable names start with a lowercase and structs start with a capital letter
public struct Page: Decodable {
private enum CodingKeys : String, CodingKey {
case count = "Count", message = "Message", searchCriteria = "SearchCriteria", cars = "Results"
}
let count: Int
let message: String
let searchCriteria: String
let cars: [Car]
}
public struct Car: Decodable {
private enum CodingKeys : String, CodingKey {
case modelYear = "ModelYear", make = "Make", model = "Model", VIN
}
let modelYear: String
let make: String
let model: String
let VIN: String
}
The cars array is in the variable result. This code prints all values
let result = try JSONDecoder().decode(Page.self, from: data)
for car in result.cars {
print("Make: \(car.make), model: \(car.model), year: \(car.modelYear), VIN: \(car.VIN)")
}

Convert string to Date/Int/Double using codable

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()