How to Decode Badly Formatted JSON in Swift using Codable - json

I am trying to decode the Yelp REST API business search response in Swift using codable. I have managed to decode this using the traditional method of serializing the response into a NSDictionary and just marshaling objects using values from the dictionary; however, as an exercise to myself I am trying to refactor the code to use the Codable protocols and JSONDecoder() in Swift. Yelp really wrote a shitty API and this is the response that I need to decode:
{
businesses = (
{
alias = "dos-caminos-new-york-7";
categories = (
{
alias = mexican;
title = Mexican;
},
{
alias = bars;
title = Bars;
}
);
coordinates = {
latitude = "40.7593727";
longitude = "-73.9853281";
};
"display_phone" = "(212) 918-1330";
distance = "55.57057440108928";
id = 2iwT3iutZvmqzmu7oOkWFw;
"image_url" = "https://s3-media2.fl.yelpcdn.com/bphoto/eHvXPqv6iLcTXfT486z6JA/o.jpg";
"is_closed" = 0;
location = {
address1 = "1567 Broadway";
address2 = "";
address3 = "";
city = "New York";
country = US;
"display_address" = (
"1567 Broadway",
"New York, NY 10036"
);
state = NY;
"zip_code" = 10036;
};
name = "Dos Caminos";
phone = "+12129181330";
price = "$$";
rating = "3.5";
"review_count" = 644;
transactions = (
delivery,
pickup
);
url = "https://www.yelp.com/biz/dos-caminos-new-york-7?adjust_creative=RzPd81IBxDPwaWBeNhRk8w&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=RzPd81IBxDPwaWBeNhRk8w";
},
{
alias = "bond-45-new-york-2";
categories = (
{
alias = italian;
title = Italian;
},
{
alias = pizza;
title = Pizza;
},
{
alias = newamerican;
title = "American (New)";
}
);
coordinates = {
latitude = "40.7591637016814";
longitude = "-73.9859788257256";
};
"display_phone" = "(212) 869-4545";
distance = "77.44246334243893";
id = "8-Ug0Zjs5sL6MARspRJ7CA";
"image_url" = "https://s3-media2.fl.yelpcdn.com/bphoto/CEfwtx0YtfDGog0hWmQ72A/o.jpg";
"is_closed" = 0;
location = {
address1 = "221 W 46th St";
address2 = "";
address3 = "";
city = "New York";
country = US;
"display_address" = (
"221 W 46th St",
"New York, NY 10036"
);
state = NY;
"zip_code" = 10036;
};
name = "Bond 45";
phone = "+12128694545";
price = "$$";
rating = "3.5";
"review_count" = 755;
transactions = (
delivery,
pickup
);
url = "https://www.yelp.com/biz/bond-45-new-york-2?adjust_creative=RzPd81IBxDPwaWBeNhRk8w&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=RzPd81IBxDPwaWBeNhRk8w";
}
);
region = {
center = {
latitude = "40.758896";
longitude = "-73.98513";
};
};
total = 5300;
}
The top level object has a value of businesses which has a list? of Businesses that I want to create. I have created the corresponding Swift Codable structs as such:
struct Response: Codable {
var businesses: [Business]
enum CodingKeys: CodingKey {
case businesses
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
// ERROR: Swift.DecodingError.typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "businesses", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))).
businesses = try values.decode([Business].self, forKey: .businesses)
print("values is \(values.allKeys)")
}
func encode(to encoder: Encoder) throws {
//TODO
}
}
and
struct Business: Codable {
var name: String
var category:
var latitude: Double = 0.0
var longitude: Double = 0.0
var phoneNumber: String = ""
var distance: Double = 0.0
var imageURL: String = ""
var price: Price
var yelpURL: String
enum CodingKeys: String, CodingKey {
case name = "name"
case category = "categories"
case coordinates = "coordinates"
case phoneNumber = "phone"
case distance = "distance"
case imageURL = "image_url"
case price = "price"
case yelpURL = "url"
}
enum CategoryCodingKey: String, CodingKey {
case alias = "alias"
case title = "title"
}
enum CoordinateInfoKeys: String, CodingKey {
case latitude = "latitude"
case longitude = "longitude"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
// TODO, will add more here but the error is not really here
}
func encode(to encoder: Encoder) throws {
}
}
The error that I get when I try to run this code is in a comment in the Response struct above. It seems to me that the YELP API is formatted in such a way that the value for businesses is not a list of objects but some other type that Swift does not recognize. How do I go about using Codable to decode this Response into the Response struct that will hold a list of Businesses? Is this even possible with the way the YELP API has been setup?

Related

Getting latitude and longitude from a json file to create a map

I wanted to modify apple's earthquakes project to display a map together with the location magnitude and time. In the json file I can see the coordinates but for the life of me I cannot read them and use them as latitude and longitude for the map. I succeeded to display the map by using the address (title) but the format changes and there are too many possibilities to account for.
The earthquake project can be downloaded at https://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed
I post the Quake.swift file below so you may have an idea of what I tried. I added a coordinates characteristic to their magnitude, place and time first as an array and then as a string but I always fail to read it and use it to display the map as latitude and longitude.
Thanks in advance for your help.
The json file is long so I post a few lines here to give you an idea of the format:
{"type":"FeatureCollection","metadata":{"generated":1648109722000,"url":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson","title":"USGS All Earthquakes, Past Month","status":200,"api":"1.10.3","count":9406},"features":[{"type":"Feature","properties":{"mag":4.5,"place":"south of the Fiji Islands","time":1648106910967,"updated":1648108178040,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/us7000gwsr","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/us7000gwsr.geojson","felt":null,"cdi":null,"mmi":null,"alert":null,"status":"reviewed","tsunami":0,"sig":312,"net":"us","code":"7000gwsr","ids":",us7000gwsr,","sources":",us,","types":",origin,phase-data,","nst":null,"dmin":5.374,"rms":1.03,"gap":102,"magType":"mb","type":"earthquake","title":"M 4.5 - south of the Fiji Islands"},"geometry":{"type":"Point","coordinates":[179.1712,-24.5374,534.35]},"id":"us7000gwsr"},
{"type":"Feature","properties":{"mag":1.95000005,"place":"2 km NE of Pāhala, Hawaii","time":1648106708550,"updated":1648106923140,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/hv72960677","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/hv72960677.geojson","felt":null,"cdi":null,"mmi":null,"alert":null,"status":"automatic","tsunami":0,"sig":59,"net":"hv","code":"72960677","ids":",hv72960677,","sources":",hv,","types":",origin,phase-data,","nst":33,"dmin":null,"rms":0.109999999,"gap":136,"magType":"md","type":"earthquake","title":"M 2.0 - 2 km NE of Pāhala, Hawaii"},"geometry":{"type":"Point","coordinates":[-155.463333129883,19.2151660919189,34.9500007629395]},"id":"hv72960677"},
{"type":"Feature","properties":{"mag":1.75,"place":"4km SE of Calabasas, CA","time":1648106545420,"updated":1648109717670,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/ci39976447","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ci39976447.geojson","felt":7,"cdi":3.1,"mmi":null,"alert":null,"status":"automatic","tsunami":0,"sig":49,"net":"ci","code":"39976447","ids":",ci39976447,","sources":",ci,","types":",dyfi,nearby-cities,origin,phase-data,scitech-link,","nst":33,"dmin":0.04554,"rms":0.27,"gap":56,"magType":"ml","type":"earthquake","title":"M 1.8 - 4km SE of Calabasas, CA"},"geometry":{"type":"Point","coordinates":[-118.61,34.1285,2.92]},"id":"ci39976447"},
The Quake.swift file:
import CoreData
import SwiftUI
import OSLog
// MARK: - Core Data
/// Managed object subclass for the Quake entity.
class Quake: NSManagedObject, Identifiable {
// The characteristics of a quake.
#NSManaged var magnitude: Float
#NSManaged var place: String
#NSManaged var time: Date
#NSManaged var coordinates: String
// A unique identifier used to avoid duplicates in the persistent store.
// Constrain the Quake entity on this attribute in the data model editor.
#NSManaged var code: String
/// Updates a Quake instance with the values from a QuakeProperties.
func update(from quakeProperties: QuakeProperties) throws {
let dictionary = quakeProperties.dictionaryValue
guard let newCode = dictionary["code"] as? String,
let newMagnitude = dictionary["magnitude"] as? Float,
let newPlace = dictionary["place"] as? String,
let newTime = dictionary["time"] as? Date,
let newCoordinates = dictionary["coordinates"] as? String
else {
throw QuakeError.missingData
}
code = newCode
magnitude = newMagnitude
place = newPlace
time = newTime
coordinates = newCoordinates
}
}
// MARK: - SwiftUI
extension Quake {
/// The color which corresponds with the quake's magnitude.
var color: Color {
switch magnitude {
case 0..<1:
return .green
case 1..<2:
return .yellow
case 2..<3:
return .orange
case 3..<5:
return .red
case 5..<Float.greatestFiniteMagnitude:
return .init(red: 0.8, green: 0.2, blue: 0.7)
default:
return .gray
}
}
/// An earthquake for use with canvas previews.
static var preview: Quake {
let quakes = Quake.makePreviews(count: 1)
return quakes[0]
}
#discardableResult
static func makePreviews(count: Int) -> [Quake] {
var quakes = [Quake]()
let viewContext = QuakesProvider.preview.container.viewContext
for index in 0..<count {
let quake = Quake(context: viewContext)
quake.code = UUID().uuidString
quake.time = Date().addingTimeInterval(Double(index) * -300)
quake.magnitude = .random(in: -1.1...10.0)
quake.place = "15km SSW of Cupertino, CA"
quake.coordinates = "-117.7153333,35.8655,7.59"
quakes.append(quake)
}
return quakes
}
}
// MARK: - Codable
/// creating or updating Quake instances.
struct GeoJSON: Decodable {
private enum RootCodingKeys: String, CodingKey {
case features
}
private enum FeatureCodingKeys: String, CodingKey {
case properties
}
private(set) var quakePropertiesList = [QuakeProperties]()
init(from decoder: Decoder) throws {
let rootContainer = try decoder.container(keyedBy: RootCodingKeys.self)
var featuresContainer = try rootContainer.nestedUnkeyedContainer(forKey: .features)
while !featuresContainer.isAtEnd {
let propertiesContainer = try featuresContainer.nestedContainer(keyedBy: FeatureCodingKeys.self)
// Decodes a single quake from the data, and appends it to the array, ignoring invalid data.
if let properties = try? propertiesContainer.decode(QuakeProperties.self, forKey: .properties) {
quakePropertiesList.append(properties)
}
}
}
}
/// A struct encapsulating the properties of a Quake.
struct QuakeProperties: Decodable {
// MARK: Codable
private enum CodingKeys: String, CodingKey {
case magnitude = "mag"
case place
case time
case code
case coordinates
}
let magnitude: Float // 1.9
let place: String // "21km ENE of Honaunau-Napoopoo, Hawaii"
let time: Double // 1539187727610
let code: String // "70643082"
let coordinates: String // [-117.7153333,35.8655,7.59]
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let rawMagnitude = try? values.decode(Float.self, forKey: .magnitude)
let rawPlace = try? values.decode(String.self, forKey: .place)
let rawTime = try? values.decode(Double.self, forKey: .time)
let rawCode = try? values.decode(String.self, forKey: .code)
let rawCoordinates = try? values.decode(String.self, forKey: .coordinates)
// Ignore earthquakes with missing data.
guard let magntiude = rawMagnitude,
let place = rawPlace,
let time = rawTime,
let code = rawCode,
let coordinates = rawCoordinates
else {
let values = "code = \(rawCode?.description ?? "nil"), "
+ "mag = \(rawMagnitude?.description ?? "nil"), "
+ "place = \(rawPlace?.description ?? "nil"), "
+ "time = \(rawTime?.description ?? "nil"), "
+ "coordinates = \(rawCoordinates?.description ?? "nil")"
let logger = Logger(subsystem: "com.example.apple-samplecode.Earthquakes", category: "parsing")
logger.debug("Ignored: \(values)")
throw QuakeError.missingData
}
self.magnitude = magntiude
self.place = place
self.time = time
self.code = code
self.coordinates = coordinates
}
// The keys must have the same name as the attributes of the Quake entity.
var dictionaryValue: [String: Any] {
[
"magnitude": magnitude,
"place": place,
"time": Date(timeIntervalSince1970: TimeInterval(time) / 1000),
"code": code,
"coordinates": coordinates
]
}
}
The coordinates are not on the same level as properties, they are in a sibling geometry. The basic pattern is
{
"features": [
{
"properties": {
"mag":1.9,
"place":"21km ENE of Honaunau-Napoopoo, Hawaii",
"time":1539187727610,"updated":1539187924350,
"code":"70643082"
},
"geometry" : {
"coordinates": [-122.8096695,38.8364983,1.96]
}
}
]
}
You have to decode the coordinates in GeoJSON by adding geometry to FeatureCodingKeys. And you have to extend the Core Data model to preserve the coordinates.
In the Core Data model add two properties
longitude - Double - non-optional, use scalar type
latitude - Double - non-optional, use scalar type
In Quake.swift
import CoreLocation
In the class Quake add
#NSManaged var latitude: CLLocationDegrees
#NSManaged var longitude: CLLocationDegrees
and replace update(from with
func update(from quakeProperties: QuakeProperties) throws {
let dictionary = quakeProperties.dictionaryValue
guard let newCode = dictionary["code"] as? String,
let newMagnitude = dictionary["magnitude"] as? Float,
let newPlace = dictionary["place"] as? String,
let newTime = dictionary["time"] as? Date,
let newLatitude = dictionary["latitude"] as? CLLocationDegrees,
let newLongitude = dictionary["longitude"] as? CLLocationDegrees
else {
throw QuakeError.missingData
}
code = newCode
magnitude = newMagnitude
place = newPlace
time = newTime
latitude = newLatitude
longitude = newLongitude
}
In the Quake extension add
var coordinate : CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
In GeoJSON extend FeatureCodingKeys
private enum FeatureCodingKeys: String, CodingKey {
case properties, geometry
}
and replace the while loop with
while !featuresContainer.isAtEnd {
let propertiesContainer = try featuresContainer.nestedContainer(keyedBy: FeatureCodingKeys.self)
// Decodes a single quake from the data, and appends it to the array, ignoring invalid data.
if var properties = try? propertiesContainer.decode(QuakeProperties.self, forKey: .properties),
let geometry = try? propertiesContainer.decode(QuakeGeometry.self, forKey: .geometry) {
let coordinates = geometry.coordinates
properties.longitude = coordinates[0]
properties.latitude = coordinates[1]
quakePropertiesList.append(properties)
}
}
Add the struct
struct QuakeGeometry: Decodable {
let coordinates : [Double]
}
In QuakeProperties add
var latitude : CLLocationDegrees = 0.0
var longitude : CLLocationDegrees = 0.0
and replace dictionaryValue with
var dictionaryValue: [String: Any] {
[
"magnitude": magnitude,
"place": place,
"time": Date(timeIntervalSince1970: TimeInterval(time) / 1000),
"code": code,
"latitude": latitude,
"longitude": longitude
]
}
Finally in DetailView.swift
import MapKit
and replace QuakeDetail with
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.3, longitudeDelta: 0.3)))
}
var body: some View {
VStack {
QuakeMagnitude(quake: quake)
Text(quake.place)
.font(.title3)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) - \(quake.longitude)")
Map(coordinateRegion: $region, annotationItems: [quake]) { item in
MapMarker(coordinate: item.coordinate, tint: .red)
}
}
}
}

How do I access integer value in JSON API?

I'm trying to retrieve an integer value from a JSON file in swift. I'm doing this as follows: self.trip.dist = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].dist) but I'm getting this error error.
Here is a link to the JSON file that I'm accessing. I'm trying to access the dist value.
These are my structures:
struct JSONStructure: Decodable {
var Trip: [TripStructure]
}
struct TripStructure: Decodable {
var LegList: LegListStructure
}
struct LegListStructure: Decodable {
var Leg: [LegStructure]
}
struct LegStructure: Decodable {
var Origin: StationStructure
var Destination: StationStructure
var Product: ProductStructure
var name: String
var type: String
var dist: Int
}
struct StationStructure: Decodable {
var time: String
var name: String
var date: String
}
struct ProductStructure: Decodable {
var catIn: String
}
// Just to condense my varibales
struct LocationInfo {
var iD = String()
var input = String()
var lat = String()
var lon = String()
var name = String()
var time = String()
var date = String()
var vehicleType = String()
var transportType = String()
var dist = String()
var legName = String()
}
Here is the function I'm using to call the function:
#Published var trip: LocationInfo = LocationInfo()
#Published var dest: LocationInfo = LocationInfo()
#Published var origin: LocationInfo = LocationInfo()
#Published var arrivalTime = String()
#Published var travelDate = String()
#Published var searchForArrival = String()
#Published var tripIndex = Int()
#Published var Trips: [Dictionary<String, String>] = []
public func FetchTrip() {
Trips.removeAll()
let tripKey = "40892db48b394d3a86b2439f9f3800fd"
let tripUrl = URL(string: "http://api.sl.se/api2/TravelplannerV3_1/trip.json?key=\(tripKey)&originExtId=\(self.origin.iD)&destExtId=\(self.dest.iD)&Date=\(self.travelDate)&Time=\(self.arrivalTime)&searchForArrival=\(self.searchForArrival)")
URLSession.shared.dataTask(with: tripUrl!) {data, response, error in
if let data = data {
do {
let decodedJson = try JSONDecoder().decode(JSONStructure.self, from: data)
self.tripIndex = decodedJson.Trip.count - 1
for i in 0..<decodedJson.Trip[self.tripIndex].LegList.Leg.count {
self.trip.transportType = decodedJson.Trip[self.tripIndex].LegList.Leg[i].type
if self.trip.transportType == "WALK" {
self.origin.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.name
self.origin.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.time.prefix(5))
self.origin.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.date
self.dest.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.name
self.dest.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.time.prefix(5))
self.dest.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.date
self.trip.vehicleType = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Product.catIn
self.trip.dist = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].dist) // This is where the problem lies
self.Trips.append(["Origin": self.origin.name, "Destination": self.dest.name, "OriginTime": self.origin.time, "DestTime": self.dest.time, "OriginDate": self.origin.date, "DestDate": self.dest.date, "TransportType": self.trip.transportType, "VehicleType": self.trip.vehicleType, "Distance": self.trip.dist])
}
else {
self.origin.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.name
self.origin.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.time.prefix(5))
self.origin.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.date
self.dest.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.name
self.dest.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.time.prefix(5))
self.dest.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.date
self.trip.vehicleType = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Product.catIn
self.trip.legName = decodedJson.Trip[self.tripIndex].LegList.Leg[i].name
self.Trips.append(["Origin": self.origin.name, "Destination": self.dest.name, "OriginTime": self.origin.time, "DestTime": self.dest.time, "OriginDate": self.origin.date, "DestDate": self.dest.date, "TransportType": self.trip.transportType, "VehicleType": self.trip.vehicleType, "LegName": self.trip.legName])
}
}
} catch {
print(error)
}
}
}.resume()
}
I am getting this error in the console:
keyNotFound(CodingKeys(stringValue: "dist", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "Trip", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "LegList", intValue: nil), CodingKeys(stringValue: "Leg", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dist\", intValue: nil) (\"dist\").", underlyingError: nil))
This site is great: https://app.quicktype.io You just paste in your json and it will write the Codable for you. QED.
You need to use an Optional Int to extract the dist values because not all legs have one.
Here's the minimum necessary to access the dist values:
func example() {
let url = URL(string: "https://api.sl.se/api2/TravelplannerV3_1/trip.json?key=40892db48b394d3a86b2439f9f3800fd&originExtId=300101416&destExtId=300101426&Date=2021-04-15&Time=08:00&searchForArrival=1")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
let travelplanner = try? JSONDecoder().decode(Travelplanner.self, from: data)
print(travelplanner as Any)
}
}
.resume()
}
struct Travelplanner: Codable {
let trip: [Trip]
enum CodingKeys: String, CodingKey {
case trip = "Trip"
}
}
struct Trip: Codable {
let legList: LegList
enum CodingKeys: String, CodingKey {
case legList = "LegList"
}
}
struct LegList: Codable {
let leg: [Leg]
enum CodingKeys: String, CodingKey {
case leg = "Leg"
}
}
struct Leg: Codable {
let origin: Station
let destination: Station
let product: Product?
let name: String
let type: String
let dist: Int?
enum CodingKeys: String, CodingKey {
case origin = "Origin"
case destination = "Destination"
case product = "Product"
case name
case type
case dist
}
}
struct Station: Codable {
let name: String
let time: String
let date: String
}
struct Product: Codable {
let catIn: String
}
I'm glad you found a solution. There are other issues with your code that I feel the need to address.
The following code has the same effect but is, IMO, much cleaner:
func fetchTrip() {
let url = URL(string: "https://api.sl.se/api2/TravelplannerV3_1/trip.json?key=40892db48b394d3a86b2439f9f3800fd&originExtId=300101416&destExtId=300101426&Date=2021-04-15&Time=08:00&searchForArrival=1")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
let decodedJson = try! JSONDecoder().decode(Travelplanner.self, from: data)
let lastLeg = decodedJson.trip.last?.legList.leg.last
self.tripIndex = decodedJson.trip.count - 1
self.trip.transportType = lastLeg?.type ?? ""
self.origin.name = lastLeg?.origin.name ?? ""
self.origin.time = lastLeg.map { String($0.origin.time.prefix(5)) } ?? ""
self.origin.date = lastLeg?.origin.date ?? ""
self.dest.name = lastLeg?.destination.name ?? ""
self.dest.time = lastLeg.map { String($0.destination.time.prefix(5)) } ?? ""
self.dest.date = lastLeg?.destination.date ?? ""
self.trip.vehicleType = lastLeg?.product?.catIn ?? ""
if let lastWalkLeg = decodedJson.trip.last?.legList.leg.last(where: { $0.type == "WALK" }) {
self.trip.dist = String(lastWalkLeg.dist ?? 0)
}
if let lastNonwalkLeg = decodedJson.trip.last?.legList.leg.last(where: { $0.type != "WALK" }) {
self.trip.legName = lastNonwalkLeg.name
}
self.trips = (decodedJson.trip.last?.legList.leg ?? [])
.map { $0.asDict }
}
}
.resume()
}
}
extension Leg {
var asDict: [String: String] {
[
"Origin": origin.name,
"Destination": destination.name,
"OriginTime": String(origin.time.prefix(5)),
"DestTime": String(destination.time.prefix(5)),
"OriginDate": origin.date,
"DestDate": destination.date,
"TransportType": type,
"VehicleType": product?.catIn ?? "",
].merging(type == "WALK" ? ["Distance": String(dist ?? 0)] : ["LegName": name], uniquingKeysWith: { $1 })
}
}
For most of the variables, you are assigning to the same var each time through the loop. This is just wasteful. Assign the values from the last element of the loop and be done with it.
Most of the work being done in the if...else... blocks are identical. Separate out the identical parts and only put the parts that care about the Leg.type in the if/else block. Do you actually use all the class properties in other methods of the class or were you just using them to make local variables in a weird way? (I'm assuming the former in the above code just in case.) If you don't need all the other properties in other methods, and the only point is to populate the trips array, you could unload most of the code (except the self.trips =... line of course.

JSON decoder The data couldn’t be read because it isn’t in the correct format

I am new to this. Somehow I am able to understand how to do this.
I am doing below, but it's giving error- The data couldn’t be read because it isn’t in the correct format.Can someone help me with this? I am stuck on this from past 4 days. I really appreciate.
import SwiftUI
import Foundation
import Combine
struct Movie: Decodable, Identifiable {
var id: Int
var video: String
var vote_count: String
var vote_average: String
var title: String
var release_date: String
var original_language: String
var original_title: String
}
struct MovieList: Decodable{
var results: [Movie]
___________
class NetworkingManager : ObservableObject{
var objectWillChange = PassthroughSubject<NetworkingManager, Never>()
#Published var movies = [Movie]()
init() {
load()
}
func load(){
let url = URL(string: "https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=<HIDDEN>")!
URLSession.shared.dataTask(with: url){ (data, response, error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([Movie].self, from: d)
DispatchQueue.main.async {
self.movies = decodedLists
}
}else {
print("No Data")
}
} catch {
print (error.localizedDescription)
}
}.resume()
}
}
This is how the response looks like:
{
"page": 1,
"results": [
{
"id": 419704,
"video": false,
"vote_count": 1141,
"vote_average": 6.2,
"title": "Ad Astra",
"release_date": "2019-09-17",
"original_language": "en",
"original_title": "Ad Astra",
"genre_ids": [
878
],
"backdrop_path": "/5BwqwxMEjeFtdknRV792Svo0K1v.jpg",
"adult": false,
"overview": "An astronaut travels to the outer edges of the solar system to find his father and unravel a mystery that threatens the survival of Earth. In doing so, he uncovers secrets which challenge the nature of human existence and our place in the cosmos.",
"poster_path": "/xJUILftRf6TJxloOgrilOTJfeOn.jpg",
"popularity": 227.167,
"media_type": "movie"
},
]
}
Code should fetch the data and hold in it the array I created. So that I can use it to display in the front end.
I had to consume the exact same API for a similar project and this is how I did it.
When calling:
let response = try JSONDecoder().decode(MovieResponse.self, from: data)
It needs to match the same properties that the JSON response returns.
Below you'll see a MovieResponse struct and the Movie class, which will list all of the properties and return types that the JSON response returns.
The type adopts Codable so that it's decodable using a JSONDecoder instance.
See this official example for more information regarding Codable.
A type that can convert itself into and out of an external representation.
Provided they match then the JSONDecoder() will work to decode the data.
ContentView.swift:
struct ContentView: View {
#EnvironmentObject var movieViewModel: MovieListViewModel
var body: some View {
MovieList(movie: self.movieViewModel.movie)
}
}
MovieListViewModel.swift:
public class MovieListViewModel: ObservableObject {
public let objectWillChange = PassthroughSubject<MovieListViewModel, Never>()
private var movieResults: [Movie] = []
var movie: MovieResults = [Movie]() {
didSet {
objectWillChange.send(self)
}
}
func load(url: String = "https://api.themoviedb.org/3/discover/movie?sort_by=popularity.desc&api_key=<HIDDEN>") {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
guard let data = data else { return }
let response = try JSONDecoder().decode(MovieResponse.self, from: data)
DispatchQueue.main.async {
for movie in response.results {
self.movieResults.append(movie)
}
self.movie = self.movieResults
print("Finished loading Movies")
}
} catch {
print("Failed to decode: ", error)
}
}.resume()
}
}
MovieResponse.swift:
struct MovieResponse: Codable {
var page: Int
var total_results: Int
var total_pages: Int
var results: [Movie]
}
public class Movie: Codable, Identifiable {
public var popularity: Float
public var vote_count: Int
public var video: Bool
public var poster_path: String
public var id: Int
public var adult: Bool
public var backdrop_path: String
public var original_language: String
public var original_title: String
public var genre_ids: [Int]
public var title: String
public var vote_average: Float
public var overview: String
public var release_date: String
enum CodingKeys: String, CodingKey {
case popularity = "popularity"
case vote_count = "vote_count"
case video = "video"
case poster_path = "poster_path"
case id = "id"
case adult = "adult"
case backdrop_path = "backdrop_path"
case original_language = "original_language"
case original_title = "original_title"
case genre_ids = "genre_ids"
case title = "title"
case vote_average = "vote_average"
case overview = "overview"
case release_date = "release_date"
}
public init(popularity: Float, vote_count: Int, video: Bool, poster_path: String, id: Int, adult: Bool, backdrop_path: String, original_language: String, original_title: String, genre_ids: [Int], title: String, vote_average: Float, overview: String, release_date: String) {
self.popularity = popularity
self.vote_count = vote_count
self.video = video
self.poster_path = poster_path
self.id = id
self.adult = adult
self.backdrop_path = backdrop_path
self.original_language = original_language
self.original_title = original_title
self.genre_ids = genre_ids
self.title = title
self.vote_average = vote_average
self.overview = overview
self.release_date = release_date
}
public init() {
self.popularity = 0.0
self.vote_count = 0
self.video = false
self.poster_path = ""
self.id = 0
self.adult = false
self.backdrop_path = ""
self.original_language = ""
self.original_title = ""
self.genre_ids = []
self.title = ""
self.vote_average = 0.0
self.overview = ""
self.release_date = ""
}
}
public typealias MovieResults = [Movie]
MovieCellViewModel.swift:
public class MovieCellViewModel {
private var movie: Movie
public init(movie: Movie) {
self.movie = movie
}
public func getTitle() -> String {
return self.movie.title
}
// add more properties or functions here
}
MovieCell.swift:
struct MovieCell: View {
var movieCellViewModel: MovieCellViewModel
var body: some View {
Text(self.movieCellViewModel.getTitle())
}
}
MovieList.swift:
struct MovieList: View {
#EnvironmentObject var movieViewModel: MovieListViewModel
var movie: MovieResults
var body: some View {
List(self.movie) { movie in
MovieCell(movieCellViewModel: MovieCellViewModel(movie: movie))
}
}
}
I had a same error, but for the properties inside the struct for JSON object doesn't match JSON file. I fixed this error by setting the property names same as JSON file properties, and in the same order.
Swift file:
struct Coin: Decodable {
var asset_id_base: String
var rates: [CoinDetail]
}
struct CoinDetail: Decodable {
var time: String
var asset_id_quote: String
var rate: Double
}
JSON File:
{
"asset_id_base": "BTC",
"rates": [
{
"time": "2020-11-08T07:50:02.2865270Z",
"asset_id_quote": "CZK",
"rate": 328886.3419989546
},
{
"time": "2020-11-08T07:51:15.0750421Z",
"asset_id_quote": "GPL2",
"rate": 92123.4454168586
},
The Movie struct doesn't match the API response. You missed at least top level, like page and results array. Here you can paste your JSON answer and get the needed struct:
// MARK: - APIAnswer
struct APIAnswer: Codable {
let page: Int
let results: [Result]
}
// MARK: - Result
struct Result: Codable {
let id: Int
let video: Bool
let voteCount: Int
let voteAverage: Double
let title, releaseDate, originalLanguage, originalTitle: String
let genreIDS: [Int]
let backdropPath: String
let adult: Bool
let overview, posterPath: String
let popularity: Double
let mediaType: String
enum CodingKeys: String, CodingKey {
case id, video
case voteCount = "vote_count"
case voteAverage = "vote_average"
case title
case releaseDate = "release_date"
case originalLanguage = "original_language"
case originalTitle = "original_title"
case genreIDS = "genre_ids"
case backdropPath = "backdrop_path"
case adult, overview
case posterPath = "poster_path"
case popularity
case mediaType = "media_type"
}
}
Then you need to use the top level struct, like:
// ...
let decodeResult = try JSONDecoder().decode([APIAnswer].self, from: d)
let movieList = decodeResult.results
// the other advice: don't just take answer and put it to the array.
// API can have errors too, so you can get the array with 2 equal id, for example
update checked your code for second time: you use
let decodeResult = try JSONDecoder().decode([Movie].self, from: d)
instead of:
let decodeResult = try JSONDecoder().decode(MovieList.self, from: d)
P.S. better to attach also the full error from Xcode in future

Getting All the values of this output json

I want to extract the names of these coffes this is an extract of my output because i have like 1000 names, and i want to take them all automatically :
results = (
{
geometry = {
location = {
lat = "-33.3979227";
lng = "-70.58503859999999";
};
viewport = {
northeast = {
lat = "-33.39783990000001";
lng = "-70.58502229999999";
};
southwest = {
lat = "-33.39795669999999";
lng = "-70.58507830000001";
};
};
};
id = 46354da06de96a36c5c44a5fa05a10f8f83f8edd;
name = "Hotel Bidasoa";
"opening_hours" = {
"open_now" = 1;
"weekday_text" = (
);
};
}
);
"place_id" = ChIJ4dfUCC7PYpYRRDkSNifrfBE;
rating = "4.7";
scope = GOOGLE;
types = (
cafe,
lodging,
food,
store,
"point_of_interest",
establishment
);
vicinity = "Avenida Vitacura 4873, Santiago, Santiago";
},
{
geometry = {
location = {
lat = "-33.37900460000001";
lng = "-70.55533029999999";
};
viewport = {
northeast = {
lat = "-33.37897230000002";
lng = "-70.5553148";
};
southwest = {
lat = "-33.37910149999999";
lng = "-70.55537679999999";
};
};
};
id = c451d2146b7a065fa1afd0ffa39353a4b1cae178;
name = "Ceibo Emporio Cafeter\U00eda";
"opening_hours" = {
"open_now" = 0;
"weekday_text" = (
);
};
and thi is my code, but only prints me the first name i want both because i have like 1000 names:
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
print (jsonResult)
if let nombre = ((jsonResult["results"]as?NSArray)?[0]as?NSDictionary)?["name"] {
print (nombre)
}
As always:
Never use NSDictionary / NSArray in Swift when parsing JSON.
Never use mutableContainers in Swift. It's completely useless.
To get all items in the results array use a loop, for convenience and readability use a type alias:
typealias JSONDictionary = [String:Any]
if let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: []) as? JSONDictionary {
print (jsonResult)
if let results = jsonResult["results"] as? [JSONDictionary] {
for result in results {
print(result["name"] as? String ?? "n/a")
if let geometry = result["geometry"] as? JSONDictionary,
let location = geometry["location"] as? JSONDictionary {
let lat = location["lat"] as? Double ?? 0.0
let lng = location["lng"] as? Double ?? 0.0
print("latitude: \(lat)")
print("longitude: \(lng)")
}
}
}
}

how to map JSON object with MapKit using swift

I am working with a MapKit example using swift. The JSON object they are using is a object of arrays. I have a JSON file that is a array of objects. from the example I see they are pulling the properties they want by there location in the array. I need to use the property keys in my objects. How do I do this? Very first time with swift. thanks
here are example files
init(title: String, locationName: String, discipline: String, coordinate: CLLocationCoordinate2D) {
self.title = title
self.locationName = locationName
self.discipline = discipline
self.coordinate = coordinate
super.init()
}
class func fromJSON(json: [JSONValue]) -> Artwork? {
// 1
var title: String
if let titleOrNil = json[16].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json[12].string
let discipline = json[15].string
// 2
let latitude = (json[18].string! as NSString).doubleValue
let longitude = (json[19].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}
var subtitle: String {
return locationName
}
// MARK: - MapKit related methods
// pinColor for disciplines: Sculpture, Plaque, Mural, Monument, other
func pinColor() -> MKPinAnnotationColor {
switch discipline {
case "Sculpture", "Plaque":
return .Red
case "Mural", "Monument":
return .Purple
default:
return .Green
}
}
// annotation callout opens this mapItem in Maps app
func mapItem() -> MKMapItem {
let addressDict = [String(kABPersonAddressStreetKey): self.subtitle]
let placemark = MKPlacemark(coordinate: self.coordinate, addressDictionary: addressDict)
let mapItem = MKMapItem(placemark: placemark)
mapItem.name = self.title
return mapItem
}
Snippet of JSON file
[
198,
"9E5E1F28-22AF-459A-841E-5E89B022505E",
198,
1340413921,
"436621",
1340413921,
"436621",
"{\n}",
null,
null,
"1933",
"Bronze plaque mounted on a stone with an inscription marking the site of an artesian well. Located along Wilder Avenue near Artesian Way.",
"1922 Wilder Avenue",
"http://hiculturearts.pastperfect-online.com/34250images/004/193301-2.JPG",
"1933.01",
"Plaque",
"Site of Honolulu's Pioneer Artesian Well",
"Full",
"21.30006",
"-157.827969",
[
null,
"21.30006",
"-157.827969",
null,
false
],
null
],
snippet of JSON file i want to use
{
"id": "301B2",
"name": "Wrenhurst",
"lat": "35.815864",
"lng": "-78.918893",
"status": "Act 2Q12",
"minp": "632000",
"maxp": "678000",
"annStarts": "14",
"annClosings": "0",
"bldList": "Builder example",
"vdl": "0",
"futures": "0",
"lotSize": "95'",
It looks like your example is using SwiftyJSON
You can look at the docs to get more information on how to get data out of a JSONValue object, but specifically, if I understand your question, you need to know how to get values using keys rather than position.
OK, it depends on what values you are getting and whether or not those values can be null in your JSON file (aka nil in Swift)
I'm going to quote the example that you gave and then show you how to get that value from your JSON file
If you know for a fact that a value will never be null in your JSON file and will always be provided:
// The code from your example using array position
let locationName = json[12].string
// Would convert to:
let locationName = json["name"].string
// And in the case of lat / lng
let latitude = (json[18].string! as NSString).doubleValue
// This converts to:
let latitude = (json["lat"].string! as NSString).doubleValue
Simply use the key instead of the position to get at your value. However, this is not the only change you need. Instead of passing an array of JSONValue objects to your factory method, you need to specify a normal JSONValue (or just a JSON object)
So your factory method changes from this:
class func fromJSON(json: [JSONValue]) -> Artwork? {
// 1
var title: String
if let titleOrNil = json[16].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json[12].string
let discipline = json[15].string
// 2
let latitude = (json[18].string! as NSString).doubleValue
let longitude = (json[19].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}
To something like this:
// This class method should return an Artwork object or nil if one couldn't be created for any reason.
// In your example, I couldn't find any reason you would return nil, so... in this example, it simply returns and Artwork object
class func fromJSON(json: JSON) -> Artwork {
// 1
var title: String
if let titleOrNil = json["title"].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json["name"].string
let discipline = json["discipline"].string
// 2
let latitude = (json["lat"].string! as NSString).doubleValue
let longitude = (json["lng"].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}