I know there are various threads on this but it seems like there's a fundamental flaw in my understanding of objects in Swift.
So I have the following function that returns a JSON response as expected:
func executeGet( completion: #escaping ([[String:Any]]?, Error?) -> Void) {
AF.request("https://myURL",
method:.get,
headers:headers).responseJSON{ response in
debugPrint(response) <- I get readable JSON response from here
if let error = response.error {
completion(nil, error)
}
else if let jsonArray = response.value as? [[String:Any]]{
completion(jsonArray, nil)
}
else if let jsonDict = response.value as? [String:Any]{
completion([jsonDict], nil )
}
}
}
And then I'm able to leverage the completion handler to read the JSON response outside of AL's scope:
executeGet() { (json, error) in
if let error = error{
print(error.localizedDescription)
}
else if let json = json {
print(type(of:json)) <- did this for sanity check
print(json["result"][0]["capturedlists"]) <- Error No exact matches in call to subscript
}
}
But I'm unable to just get the 'capturedLists' part of the JSON. The JSON snippet I need to capture looks as follows:
"capturedLists":{"Something":
[{"Position":"1","A1":"a1","B1":"b1"},
{"Position":"2","A2":"a2","B2":"b2"}]}}}
Can anyone help me figure out what am I doing wrong? I know usually you can get data from Json by something like response[a][0] but that doesn't work here for some reason.
EDIT: Upone suggestion I used Quicktype to get a struct for my response and got the following:
struct Welcome: Codable {
let statusCode: Int
let messageCode: String
let result: Result
}
// MARK: - Result
struct Result: Codable {
let id: String
let inputParameters: InputParameters
let robotID: String
let runByUserID, runByTaskMonitorID: JSONNull?
let runByAPI: Bool
let createdAt, startedAt, finishedAt: Int
let userFriendlyError: JSONNull?
let triedRecordingVideo: Bool
let videoURL: String
let videoRemovedAt: Int
let retriedOriginalTaskID: String
let retriedByTaskID: JSONNull?
let capturedDataTemporaryURL: String
let capturedTexts: CapturedTexts
let capturedScreenshots: CapturedScreenshots
let capturedLists: CapturedLists
enum CodingKeys: String, CodingKey {
case id, inputParameters
case robotID = "robotId"
case runByUserID = "runByUserId"
case runByTaskMonitorID = "runByTaskMonitorId"
case runByAPI, createdAt, startedAt, finishedAt, userFriendlyError, triedRecordingVideo
case videoURL = "videoUrl"
case videoRemovedAt
case retriedOriginalTaskID = "retriedOriginalTaskId"
case retriedByTaskID = "retriedByTaskId"
case capturedDataTemporaryURL = "capturedDataTemporaryUrl"
case capturedTexts, capturedScreenshots, capturedLists
}
}
.responseJSON is deprecated. Don't use it.
The error occurs because json["result"] returns Any but the compiler must know the static type for the subsequent index subscription.
As you have a Decodable model AF provides responseDecodable. Change your function to
func executeGet( completion: #escaping (Result<Welcome,Error>) -> Void) {
AF.request("https://myURL",
method:.get,
headers:headers).responseDecodable(of: Welcome.self) { response in
switch response.result {
case .success(let welcome):
completion(.success(welcome))
case .failure(let error):
completion(.failure(error))
}
}
}
which can even be simplified to
func executeGet( completion: #escaping (Result<Welcome,Error>) -> Void) {
AF.request("https://myURL",
method:.get,
headers:headers).responseDecodable(of: Welcome.self) { response in
completion(response.result)
}
}
And call it
executeGet() { result in
switch result {
case .success(let welcome): print(welcome)
case .failure(let error): print(error)
}
}
If you get an error the model doesn't match the JSON and the error tells you exactly what's wrong
Related
I have a JSON response from an api call. The problem is I get different JSON responses depending on whether the user has entered the correct credentials or not. My question is how do I read and decode these responses to a useable struct and what is the best way to go about decoding these different responses. one thing I noticed is both response have a common "isSuccess" that may be useful. I have little to no experience with swift or reading JSON so this is all a learning experience for me.
This is the response for successful login
{"result":{"login":{"isAuthorized":true,"isEmpty":false,"userName":{"isEmpty":false,"name":{"firstName":"Jason","lastName":"Test","displayName":"Test, Jason","isEmpty":false,"fullName":"Jason Test"},"canDelete":false,"id":5793,"canModify":false},"username":"test#testable.com"},"parameters":{"isEmpty":false,"keep_logged_in_indicator":false,"username":"test#testable.com"}},"isAuthorized":true,"version":{"major":"2021","minor":"004","fix":"04","display":"2021.004.04","isEmpty":false},"isSystemDown":false,"timestamp":"2021-07-28T02:47:33Z","isSuccess":true}
This is the response for failure
{"isAuthorized":true,"version":{"major":"2021","minor":"004","fix":"04","display":"2021.004.04","isEmpty":false},"isSystemDown":false,"errors":[{"password":"Unable to login as 'test#testable.com'"}],"timestamp":"2021-07-28T02:47:05Z","isSuccess":false}
This is the code I have written for my api calls
func request<T: Decodable>(endPoint: EndPoint, method: Method, parameters: [String: Any]? = nil, completion: #escaping(Result<T, Error>) -> Void) {
// Creates a urlRequest
guard let request = createRequest(endPoint: endPoint, method: method, parameters: parameters) else {
completion(.failure(AppError.invalidUrl))
return
}
let session = URLSession.shared
session.dataTask(with: request) { data, response, error in
var results: Result<Data, Error>?
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
completion(.failure(AppError.badStatusCode))
return
}
if let response = response {
// Gets the JSESSIONID
let cookieName = "JSESSIONID"
if let cookie = HTTPCookieStorage.shared.cookies?.first(where: { $0.name == cookieName }) {
debugPrint("\(cookieName): \(cookie.value)")
}
print(response)
}
if let data = data {
results = .success(data)
// Converts data to readable String
let responseString = String(data: data, encoding: .utf8) ?? "unable to convert to readable String"
print("Server Response: \(responseString.description)")
} else if let error = error {
results = .failure(error)
print("Server Error: \(error.localizedDescription)")
}
DispatchQueue.main.async {
self.handleResponse(result: results, completion: completion)
}
}.resume()
}
private func handleResponse<T: Decodable>(result: Result<Data, Error>?, completion: (Result<T, Error>) -> Void) {
guard let result = result else {
completion(.failure(AppError.unknownError))
return
}
switch result {
case .success(let data):
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
print("Server JsonObject response: \(json)")
} catch {
completion(.failure(AppError.errorDecoding))
}
let decoder = JSONDecoder()
// Decodes that json data
do {
} catch {
}
case .failure(let error):
completion(.failure(error))
}
}
Im mostly interesting in being able to display the json error that occurs when credentials are incorrect. The deadline for my project Is slowing approaching and any help or suggestions would be much appreciated.
You can use Swift's Result type to differentiate a successful result from a failed result.
The Result type is not decodable by default so you will need to write a custom decoder like this:
struct Response: Decodable {
let result: Swift.Result<Result, Errors>
enum CodingKeys: String, CodingKey {
case isSuccess
case errors
case result
}
struct Result: Codable {
let login: Login
struct Login: Codable {
let isAuthorized: Bool
}
}
struct Errors: Error {
let contents: [[String: String]]
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if try container.decode(Bool.self, forKey: .isSuccess) {
result = .success(try container.decode(Result.self, forKey: .result))
} else {
result = .failure(
Errors(contents: try container.decode([[String: String]].self, forKey: .errors))
)
}
}
}
The JSON from server looks like this:
A dictionary where the value is another dictionary.
{
"S1": {
"vpn_status": 2,
"vpn_name": "vpn1"
},
"S2": {
"vpn_status": 1,
"vpn_name": "vpn2"
}
}
I have created the following struct to parse it.
public struct ServerStatusResult {
public let vpnName: String
public let status: Int
init?(json: [String: Any]) {
guard
let vpnName = json["vpn_name"] as? String,
let status = json["vpn_status"] as? Int
else {
return nil
}
self.vpnName = vpnName
self.status = status
}
}
And the function to call the server is:
typealias serverStatusCompletedClosure = (_ status: Bool, _ result: Dictionary<String,ServerStatusResult>?, _ error: ServiceError?)->Void
func serverStatus(email: String, password: String, complete: #escaping serverStatusCompletedClosure) {
let url = URL(string: "...")!
try? self.httpClient.get(url: url,
token: "...",
email: email,
password: password)
{ (data, response, error) in
if let error = error {
complete(false, nil, ServiceError.invalidSession)
} else if let httpResponse = response as? HTTPURLResponse {
switch (httpResponse.statusCode) {
case 200:
var result: [String:ServerStatusResult]? = nil
result = try! JSONSerialization.jsonObject(with: data!, options: []) as! Dictionary<String, ServerStatusResult>
complete(true, result, nil)
This is where my json transformation fails.
Could not cast value of type '__NSDictionaryI' (0x7fff8eaee9b0) to
'app.ServerStatusResult' (0x10021dec0).
What am I missing please?
You can solve it by using Decodable and using a dictionary
First make your struct conform to Decodable
public struct ServerStatusResult: Decodable {
public let vpnName: String
public let status: Int
enum CodingKeys: String, CodingKey {
case vpnName = "vpn_name"
case status = "vpn_status"
}
}
and then the decoding is easy
do {
let result = try JSONDecoder().decode([String: ServerStatusResult].self, from: data)
print(result) //or in you case complete(true, result, nil)
} catch {
print(error)
}
You get an array of dictionary [[String: Any]]
Create a struct for a dictionary and if a dictionary has another dictionary inside it then create another struct for the inner Dictionary and create an object in the outer struct for innner dictionary json.
You can use codeable to parse your json easily, by inheriting your struct with codeable
I have a problem with my alamofire.request. The response is nil but I don't understand why.
My problem are: I use API of OpenFoodFacts, it works with a web address of this style: https://fr.openfoodfacts.org/api/v0/produit/3366321054229
I use Alamofire to get the request with a protocol, a session and a service.
My Protocol:
protocol CheckerFridgeProtocol {
func request(url: URL, parameters: Parameters, callback: #escaping (DataResponse<Any>) -> Void )
}
My Session:
class CheckerFridgeSession: CheckerFridgeProtocol {
func request(url: URL, parameters: Parameters, callback: #escaping (DataResponse<Any>) -> Void) {
Alamofire.request(url, method: .get, parameters: parameters).responseJSON { responseData in
callback(responseData)
}
}
}
My service:
class CheckerFridgeService {
private var checkerFridgeSession: CheckerFridgeSession
init(checkerFridgeSession: CheckerFridgeSession = CheckerFridgeSession()) {
self.checkerFridgeSession = checkerFridgeSession
}
// get recipe ingredients
func getRequest(barCode: String, callback: #escaping (Bool, CheckerFridgeData?) -> Void) {
let parameters: Parameters = [:]
let url = URL(string: "https://fr.openfoodfacts.org/api/v0/produit/\(barCode)")!
checkerFridgeSession.request(url: url, parameters: parameters) { (response) in
DispatchQueue.main.async {
guard response.response?.statusCode == 200 else {
print("Response 400 Error")
callback(false, nil)
return
}
guard let data = response.data else {
print("Data error")
callback(false, nil)
return
}
guard let responseJSON = try? JSONDecoder().decode(CheckerFridgeData.self, from: data) else {
print("Response JSON Failed")
callback(false, nil)
return
}
guard responseJSON.status > 0 else {
print("Status Code 0")
callback(false, nil)
return
}
// print(response.result.value as Any)
callback(true, responseJSON)
}
}
}
}
I have a file CheckerFridgeData contain Struct to decode the responseJSON:
// MARK: - CheckerFridge
struct CheckerFridgeData: Codable {
let status: Int
let product: Product?
}
// MARK: - Product
struct Product: Codable {
let additivesTags: [String]?
let productNameFr: String?
let imageSmallURL, imageFrontSmallURL: String?
}
When I use my request to my code, I have no error but my responseJSON return nil
CheckerFridgeData(status: 1, product: Optional(Checker_Fridge.Product(additivesTags: nil, productNameFr: nil, imageSmallURL: nil, imageFrontSmallURL: nil)))
But if I print response.result.value as Any the code returns the JSON Value.
Have you any idea why I can't decode my struct with my JSON data ?
Thanks a lot for your help
I think the problem is about custom types. If you'd like to convert custom types you need to use CodingKeys protocol. When I looked your json there is no property like additivesTags there is additives_tags so you should use CodingKeys like following.
struct Product: Codable {
let additivesTags: [String]?
let productNameFr: String?
let imageSmallURL, imageFrontSmallURL: String?
enum CodingKeys: String, CodingKey {
case additivesTags = "additive_tags"
case productNameFr = "product_name_fr"
case imageSmallURL
}
}
I've been using app.quicktype.io to generate the code to decode JSON. I'm facing a problem now that I have not seen before.
First, I have this simple struct:
import Foundation
struct GenericDM: Codable {
let status, statusMessage: String
let result: [Result]
enum CodingKeys: String, CodingKey {
case status
case statusMessage = "status_message"
case result
}
}
struct Result: Codable {
let applicationID, applicationName, applicationType, suFirstName: String
let suMiddleName, suLastName, suAge, suDob: String
let suRace: String
let suAddress: SuAddress
let createdTime, updatedTime: Int
enum CodingKeys: String, CodingKey {
case applicationID = "application_id"
case applicationName = "application_name"
case applicationType = "application_type"
case suFirstName = "su_first_name"
case suMiddleName = "su_middle_name"
case suLastName = "su_last_name"
case suAge = "su_age"
case suDob = "su_dob"
case suRace = "su_race"
case suAddress = "su_address"
case createdTime = "created_time"
case updatedTime = "updated_time"
}
}
struct SuAddress: Codable {
let addrLine1, addrLine2, stName, addrCity: String
let addrState, addrCounty, addrPin: String
enum CodingKeys: String, CodingKey {
case addrLine1 = "addr_line_1"
case addrLine2 = "addr_line_2"
case stName = "st_name"
case addrCity = "addr_city"
case addrState = "addr_state"
case addrCounty = "addr_county"
case addrPin = "addr_pin"
}
}
func newJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
decoder.dateDecodingStrategy = .iso8601
}
return decoder
}
func newJSONEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
encoder.dateEncodingStrategy = .iso8601
}
return encoder
}
// MARK: - URLSession response handlers
extension URLSession {
fileprivate func codableTask<T: Codable>(with url: URL, completionHandler: #escaping (T?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
return self.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
completionHandler(nil, response, error)
return
}
completionHandler(try? newJSONDecoder().decode(T.self, from: data), response, nil)
}
}
func genericDMTask(with url: URL, completionHandler: #escaping (GenericDM?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
return self.codableTask(with: url, completionHandler: completionHandler)
}
}
Then, I read a JSON file and try to decode the data from a different class:
let bundle = Bundle.main
let path = bundle.path(forResource: "MockGenericData", ofType: "json")
let jsonData = try? String.init(contentsOf: URL.init(fileURLWithPath: path!))
let genericDM = try? newGenericDMJSONDecoder().decode(GenericDM.self, from: jsonData)
I temporarily need to read from that mockup file before I can get it from the backend.
However, I'm getting Cannot invoke 'decode' with an argument list of type '(GenericDM.Type, from: String?)' and I don't understand why.
Anybody has any idea?
Assuming decode is the normal Decoder decode method. The from parameter expect Data, not an optional String.
let url = Bundle.main.url(forResource: "MockGenericData", withExtension: "json")!
do {
let jsonData = try Data(contentsOf: url)
let genericDM = try newGenericDMJSONDecoder().decode(GenericDM.self, from: jsonData)
} catch {
print(error)
}
I'd like to make a function that takes in a few parameters and then outputs the data I need from a web API. Obviously a good deal of the time I'll need to customize it to suit the use case but just for fun I'm trying to figure out a super basic function the successfully parses JSON, as about half of the lines of code in the function below are generic error handling.
For example if I generally use something like
func getJSON(completionHandler: #escaping (Bool) -> ()) {
let jsonUrlString = "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808"
guard let url = URL(string: jsonUrlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data, err == nil else {
print(err!)
return
}
do {
let response = try
JSONDecoder().decode(TopStoriesResponse.self, from: data)
self.storyData = response.results
completionHandler(true)
} catch let jsonErr {
print("Error serializing JSON", jsonErr)
}
}.resume()
}
The only three things that will change from case to case (again, in the most absolutely basic of scenarios) are the url link to the API, the Struct that I set up to look for the pieces of data I need, and the array that I output the results to once the data request is finished.
Could I trim the fat on that and do something like
func jsonFetcher(apiLink: String, structToDecode: String, arrayThatHoldsResponse: [String], completionHandler: #escaping (Bool) -> ()) {
let jsonUrlString = apiLink
guard let url = URL(string: jsonUrlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data, err == nil else {
print(err!)
return
}
do {
let response = try
JSONDecoder().decode(structToDecode, from: data)
arrayThatHoldsResponse = response.results
completionHandler(true)
} catch let jsonErr {
print("Error serializing JSON", jsonErr)
}
}.resume()
}
I'm just not sure about the data types of structToDecode and arrayThatHoldsResponse (in the example function above I just using String as a placeholder), assuming they look like
Struct(s)
struct TopStoriesResponse: Decodable {
let status: String
let results: [Story]
}
struct Story: Decodable {
let title: String
let abstract: String
let url: String
let multimedia: [Multimedia]
private enum CodingKeys: String, CodingKey {
case title
case abstract
case url
case multimedia
}
init(from decoder:Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
abstract = try container.decode(String.self, forKey: .abstract)
url = try container.decode(String.self, forKey: .url)
multimedia = (try? container.decode([Multimedia].self, forKey: .multimedia)) ?? []
}
}
Array
var storyData = [Story]()
This way I can just call
jsonFetcher(apiLink: link, structToDecode: myStruct, arrayThatHoldsResponse: myArray, completionHandler: <#T##(Bool) -> ()#>)
Thanks for any help!
The power of generics. You can make a generic function, where the parameter is the urlString. The T inheritance the Decodable protocol.
This way you can call this function everytime as long as your Model inheritance the Decodable protocol.
func fetchData<T: Decodable>(urlString: String, completion: #escaping (T) -> ()) {
let url = URL(string: urlString)!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
}
guard let data = data else { return }
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(object)
} catch let jsonErr {
print("Failed to decode json:", jsonErr)
}
}.resume()
}
How to call the function:
struct User: Decodable { }
fetchData(urlString: "yourUrl") { (User: User) in
// Handle result
}
struct Animal: Decodable { }
fetchData(urlString: "yourUrl") { (animal: Animal) in
// Handle result
}
// Or if you want to fetch an array of users instead
fetchData(urlString: "yourUrl") { (users: [User]) in
// Handle result
}
In your case
var storiesData: [Story] = []
fetchData(urlString: "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808") { (stories: [Story] in
storiesData = stories
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Based on Jacob's answer I recommend to return also a possible error.
To keep the generic layout declare an – also generic – enum as return type
enum FetchResult<T> {
case success(T), failure(Error)
}
and return FetchResult with the passed static type
func fetchData<T: Decodable>(url: URL, completion: #escaping (FetchResult<T>) -> Void) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {completion(.failure(error!)); return }
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(.success(object))
} catch {
completion(.failure(error))
}
}.resume()
}
and use it
let jsonUrl = URL(string: "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=••••••••••••••••••:1:73741808")!
fetchData(url: jsonUrl) { (result : FetchResult<TopStoriesResponse>) in
switch result {
case .success(let object): print(object) // do something with object
case .failure(let error): print(error) // handle the error
}
}