I can't parse a json response from an Alamofire query into a model. I have this model code. What am I doing wrong? I am using Swift 3 in Xcode 8.3
enum SerializationError: Error {
case missing(String)
case invalid(String, Any)
}
struct Thing {
var id: String
var name: String
}
extension Thing {
init(json: [String: Any]) throws {
guard let id = json["id"] as? String else {
throw SerializationError.missing("id")
}
guard let name = json["name"] as? String else {
throw SerializationError.missing("name")
}
self.id = id
self.name = name
}
}
Then in my controller I have
func parseData(jsonData: [String: Any]) {
var model = [Thing]()
let things = jsonData["things"] as! [[String: Any]]
for thing in things {
do {
let aThing = try Thing(json: thing)
model.append(aThing)
} catch let error {
print(error.localizedDescription)
}
}
}
I always get an error. I know that the error isn't about the json response as I have checked it carefully and had extra code in there to test that the elements are present.
The operation couldn’t be completed. (MyApp.SerializationError error 0.)
Related
I am new to SwiftUI and I am trying to encode and decode a MKPlacemark struct to json.
I have the struct defined as below. I am able to display the details in the app but I am not able to decode it.
import Foundation
import MapKit
import UIKit
struct Landmark {
let placemark: MKPlacemark
var id: UUID {
return UUID()
}
var name: String {
self.placemark.name ?? ""
}
var title: String {
self.placemark.title ?? ""
}
var coordinate: CLLocationCoordinate2D {
self.placemark.coordinate
}
}
I can search for placemarks like this:
import Foundation
import Combine
import MapKit
class SearchPlaces: NSObject, ObservableObject {
#Published var searchQuery = ""
#Published var landmarks: [Landmark] = [Landmark]()
#Published var items: [MapItem] = [MapItem]()
public func getNearByLandmarks() {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = searchQuery
let search = MKLocalSearch(request: request)
search.start { (response, error) in
if let response = response {
let mapItems = response.mapItems
self.landmarks = mapItems.map {
Landmark(placemark: $0.placemark)
}
Task {
await self.getData()
}
print("Lamdmarks \(self.landmarks)")
}
}
}
private func getData() async {
guard let landmark = try? JSONEncoder().encode(self.landmarks) else { return }
do {
let decodedLandmark = try JSONDecoder().decode(Landmark.self, from: landmark)
print("decodedLandmark \(decodedLandmark.id)")
} catch {
print("Error \(error.localizedDescription)")
}
}
}
But I get this error: Error
The data couldn’t be read because it isn’t in the correct format.
The placemark looks like this in xcode
Lamdmarks \[Landmark(placemark: La Hacienda Market, 249 Hillside Blvd, South San Francisco, CA 94080, United States # \<+37.66312925,-122.40844847\> +/- 0.00m, region CLCircularRegion (identifier:'\<+37.66307481,-122.40861130\> radius 141.17', center:\<+37.66307481,-122.40861130\>, radius:141.17m))
How do I decode a MKPlacemark to json when I don't know all of its keys.
I tried this
extension NSSecureCoding { func archived() throws -> Data { try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) } }
extension Data { func unarchived<T: NSSecureCoding>() throws -> T? { try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(self) as? T } }
extension Landmark: Codable {
func encode(to encoder: Encoder) throws {
var unkeyedContainer = encoder.unkeyedContainer()
try unkeyedContainer.encode(placemark.archived())
try unkeyedContainer.encode(id)
try unkeyedContainer.encode(name)
try unkeyedContainer.encode(title)
try unkeyedContainer.encode(coordinate)
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
placemark = try container.decode(Data.self).unarchived()!
coordinate = try container.decode(CLLocationCoordinate2D.self, "coordinate")
id = try container.decode(UUID.self)
name = placemark.name ?? "no name"
title = placemark.title ?? "no title"
}
}
First of all never print(error.localizedDescription) in a Codable context. The generic error message is meaningless.
Always
print(error)
to get the real meaningful DecodingError.
Second of all don't try to adopt Codable by serializing each single property in classes which conform to NSSecureCoding. Take advantage of the built-in serialization and also of the PropertyWrapper pattern.
This PropertyWrapper converts/serializes MKPlacemark to Data and vice versa
#propertyWrapper
struct CodablePlacemark {
var wrappedValue: MKPlacemark
}
extension CodablePlacemark: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
guard let placemark = try NSKeyedUnarchiver.unarchivedObject(ofClass: MKPlacemark.self, from: data) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid placemark"
)
}
wrappedValue = placemark
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true)
try container.encode(data)
}
}
In the Landmark struct adopt Codable and declare the placemark
struct Landmark: Codable {
#CodablePlacemark var placemark: MKPlacemark
}
But the property wrapper makes only sense if you encode the placemark.
I have a view with a button that calls an API, the API either returns an HTTP code 200 or 400 based on a particular scenario.
The button works just fine and everything works smoothly if code 200 is returned, however if code 400 is returned, the view is not updated that the user have to click on the button once again to get the updated message.
I added the http code property as a published variable in the VM's class and the http is an observable, but it doesn't get updated in the view on the first API call, I'm not sure what I'm missing.
I made a lot of changes to the shared code just to help in demonstrating the actual problem.
Update: Also I think another part of the problem, is that the url function returns the value before the url session returns the data, I don't know why this is happening, that when I execute it a second time it uses the values from the previous execution.
HTTPError Class
class HTTPError : Codable, ObservableObject {
var statusCode: Int?
var message: [String]?
var error: String?
init(statusCode: Int? = nil, message: [String]? = [], error: String? = nil){
self.statusCode = statusCode
self.message = message ?? []
self.error = error
}
convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.statusCode = try container.decodeIfPresent(Int.self, forKey: .statusCode)
do {
self.message = try container.decodeIfPresent([String].self, forKey: .message)
} catch {
guard let value = try container.decodeIfPresent(String.self, forKey:
.message) else {return}
self.message = []
self.message?.append(value)
}
self.error = try container.decodeIfPresent(String.self, forKey: .error)
}
VM Class
class VM: ObservableObject {
#Published var isLoading = true
#Published var httpError = HTTPError()
func checkDriverIn(_ record: DriverQRParam) async -> (Bool) {
...
var request = URLRequest(url: url)
...
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
...
let task = URLSession.shared.dataTask(with: request) { (data, response,
error) in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
self.httpError = try! JSONDecoder().decode(HTTPError.self, from: data)
//gets updated just fine in this class//
}
task.resume()
}catch {
print("Couldn't encode data \(String(describing: error))")
}
if httpError.statusCode != nil && httpError.statusCode == 400 {
return (false)
} else {
return (true)
}
}
View.Swift
struct xyz: View {
#State private var VM = VM()
Button("click") {
Task {
await VM.checkDriverIn(driverParam)
}
}
}
I have an async/await function to make sure that the data gets passed along first;
func downloadFirebaseData() async -> String {
let group = DispatchGroup()
group.enter() // stop the thread/enter the function
let db = Firestore.firestore()
withUnsafeThrowingContinuation { continuation in
db.collection("annotations")
.getDocuments { (querySnapshot, error) in
defer {
group.leave() // << end on any return
}
if let Lng = i.document.get("lng") as? String {
DispatchQueue.main.async {
annotationLng.append(Lng) //edit the array
print("downloadLngServerData ()\(annotationLng)")
}
}
if let Lat = i.document.get("lat") as? String {
DispatchQueue.main.async {
annotationLat.append(Lat) //edit the array
print("downloadLatServerData ()\(annotationLat)")
}
}
}
}
group.wait() // clear up the thread now, exit the function
}
And its called under my view with;
.task {
try await downloadFirebaseData() //error 1
}
#State annotationLat: [String] = []
#State annotationLng: [String] = []
Inside of firebase database:
annotationLat = ["42.828392","29.18273","97.27352"]
annotationLng = ["42.828392","29.18273","97.27352"]
I have 2 errors;
Invalid conversion from throwing function of type '#Sendable () async throws -> Void' to non-throwing function type '#Sendable () async -> Void'
This was under the .task
My second error:
Generic parameter 'T' could not be inferred
This was under withUnsafeThrowingContinuation
The first error I somewhat get, but even after I modified from my original code, the error still persisted.
For the second error, I know that I might have to define that this is a string somewhere, because I don't think that the app knows that I'm trying to work with a string.
This assumes that the Firestore path for the documents is
annotations/{id}
and that each document has variables lat and lng of type String
import Foundation
import FirebaseFirestoreSwift
import FirebaseFirestore
import CoreLocation
//struct to keep the latitude and longitude together, they should not be in separate arrays
struct Annotation: Codable, Identifiable{
#DocumentID var id: String?
var lat: String?
var lng: String?
}
extension Annotation{
//Safely unwrap the Strings into doubles and then create the coordinate
var coordinate: CLLocationCoordinate2D? {
guard let latStr = lat, let lngStr = lng, let latitude = Double(latStr), let longitude = Double(lngStr) else{
print("Unable to get valid latitude and longitude")
return nil
}
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return coordinate
}
}
struct CustomFirestoreService{
let store: Firestore = .firestore()
init(){}
func getAnnotations() async throws -> [Annotation]{
let ANNOTATIONS_PATH = "annotations"
return try await retrieve(path: ANNOTATIONS_PATH)
}
///retrieves all the documents in the collection at the path
private func retrieve<FC : Codable>(path: String) async throws -> [FC]{
//Firebase provided async await.
let querySnapshot = try await store.collection(path).getDocuments()
return querySnapshot.documents.compactMap { document in
do{
return try document.data(as: FC.self)
}catch{
print(error)
return nil
}
}
}
}
Then in your View
import SwiftUI
struct AnnotationsView: View {
let service: CustomFirestoreService = CustomFirestoreService()
#State private var annotations: [Annotation] = []
var body: some View {
if annotations.isEmpty{
Text("Hello, World!")
.task {
do{
annotations = try await service.getAnnotations()
//Do any other work here, this line won't run unless the annotations are populated.
}catch{
print(error)
}
}
}else{
List(annotations){ annotation in
if let coord = annotation.coordinate{
VStack{
Text("Latitude = \(coord.latitude)")
Text("Longitude = \(coord.longitude)")
}
}else{
Text("Invalid Coordinate Value. Check firestore values for document \(annotation.id ?? "no id")")
}
}
}
}
}
struct AnnotationsView_Previews: PreviewProvider {
static var previews: some View {
AnnotationsView()
}
}
This makes some assumptions but if you paste it into your project you should get some working code.
You don't need this for your code but this is what a conversion from the "old" closures to the new async await would look like.
public func retrieve<FC : Codable>(path: String) async throws -> [FC]{
typealias MyContinuation = CheckedContinuation<[FC], Error>
return try await withCheckedThrowingContinuation { (continuation: MyContinuation) in
store.collection(path)
.getDocuments() { (querySnapshot, err) in
if let err = err {
//This throws an error
continuation.resume(throwing: err)
} else {
let array = querySnapshot?.documents.compactMap { document in
try? document.data(as: FC.self)
} ?? []
//This returns an array
continuation.resume(returning: array)
}
}
}
}
If you aren't calling continuation there is no point in returning a continuation of any kind.
I need help with currency exchange rate lookup given a key (3 digit currency code). The JSON object is rather unusual with no lablels such as date, timestamp, success, or rate. The first string value is the base or home currency. In the example below it is "usd" (US dollars).
I would like to cycle through all the currencies to get each exchange rate by giving its 3 digit currency code and storing it in an ordered array.
{
"usd": {
"aed": 4.420217,
"afn": 93.3213,
"all": 123.104693,
"amd": 628.026474,
"ang": 2.159569,
"aoa": 791.552347,
"ars": 111.887966,
"aud": 1.558363,
"awg": 2.164862,
"azn": 2.045728,
"bam": 1.9541,
"bbd": 2.429065,
"bch": 0.001278
}
}
In a slightly different formatted JSON object I used the following loop to copy exchange rates to an ordered array.
for index in 0..<userData.rateArray.count {
currencyCode = currCode[index]
if let unwrapped = results.rates[currencyCode] {
userData.rateArray[index] = 1.0 / unwrapped
}
}
The follow code is the API used to get the 3 digit currency codes and the exchange rates (called via UpdateRates).
class GetCurrency: Codable {
let id = UUID()
var getCurrencies: [String : [String: Double]] = [:]
required public init(from decoder: Decoder) throws {
do{
print(#function)
let baseContainer = try decoder.singleValueContainer()
let base = try baseContainer.decode([String : [String: Double]].self)
for key in base.keys{
getCurrencies[key] = base[key]
}
}catch{
print(error)
throw error
}
}
}
class CurrencyViewModel: ObservableObject{
#Published var results: GetCurrency?
#Published var selectedBaseCurrency: String = "usd"
func UpdateRates() {
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = selectedBaseCurrency // usd, eur, cad, etc
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do{
let decodedResponse = try JSONDecoder().decode(GetCurrency.self, from: data)
DispatchQueue.main.async {
self.results = decodedResponse
// this prints out the complete table of currency code and exchange rates
print(self.results?.getCurrencies["usd"] ?? 0.0)
}
} catch {
//Error thrown by a try
print(error)//much more informative than error?.localizedDescription
}
}
if error != nil {
//data task error
print(error!)
}
}.resume()
}
}
Thanks lorem ipsum for your help. Below is the updated ASI logic that copies the exchange rates to the rateArray using key/value lookups.
class CurrencyViewModel: ObservableObject{
#Published var results: GetCurrency?
#Published var rateArray = [Double] ()
init() {
if UserDefaults.standard.array(forKey: "rates") != nil {
rateArray = UserDefaults.standard.array(forKey: "rates") as! [Double]
}else {
rateArray = [Double] (repeating: 0.0, count: 160)
UserDefaults.standard.set(self.rateArray, forKey: "rates")
}
}
func updateRates(baseCur: String) {
...
DispatchQueue.main.async {
self.results = decodedResponse
// loop through all available currencies
for index in 0..<currCode.count {
currencyCode = currCode[index]
// spacial handling for base currency
if currencyCode == baseCur {
self.rateArray[index] = 1.0000
} else {
let homeRate = self.results?.getCurrencies[baseCur]
// complement and save the exchange rate
if let unwrapped = homeRate?[currencyCode] {
self.rateArray[index] = 1.0 / unwrapped
}
}
}
}
} catch {
//Error thrown by a try
print(error)//much more informative than error?.localizedDescription
}
}
if error != nil {
//data task error
print(error!)
}
}.resume()
}
}
i am new to swift programming, i have spent considerable amount of time figuring out how to parse json response from alamofire server call. My Json response is
{"customer_info":[{"customer_id":"147","response_code":1}]}
and i want to access both variables. My swift code is
Alamofire.request(
URL_USER_REGISTER,
method: .post,
parameters: parameters,
encoding: JSONEncoding.default).responseJSON
{
if let json = response.result.value {
print (json)
}
if let result = response.result.value as? [String:Any] {
var names = [String]()
do {
if let data = data,
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let blogs = json["customer_info"] as? [[String: Any]] {
for blog in blogs {
if let name = blog["customer_id"] as? String {
names.append(name)
}
}
}
} catch {
print("Error deserializing JSON: \(error)")
}
print(names)
}
}
please help
Your code is parsing correctly. Add the following code to your blog loop and get the second variable out
if let response_code = blog["response_code"] as? Int {
//Do something here
}
So the complete code you are looking for is
let str = "{\"customer_info\":[{\"customer_id\":\"147\",\"response_code\":1}]}"
let data = str.data(using: .utf8)
do {
if let data = data,
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let blogs = json["customer_info"] as? [[String: Any]] {
for blog in blogs {
if let name = blog["customer_id"] as? String {
print(name)
}
if let response_code = blog["response_code"] as? Int {
print(response_code)
}
}
}
} catch {
print("Error deserializing JSON: \(error)")
}
i have modified the code and getting result now
if let jsonDict = response.result.value as? [String:Any],
let dataArray = jsonDict["customer_info"] as? [[String:Any]]
{
let nameArray = dataArray.flatMap { $0["customer_id"] as? String }
let nameArray2 = dataArray.flatMap { $0["response_code"] as? Int }
if(dataArray.count>0)
{
//store return customer id and response code
let customer_id_received = nameArray[0]
let response_code_received = nameArray2[0]
if(response_code_received==1)
{
//proceed with storing customer id in global variable
print(nameArray2[0])
}