Array vs Dictionary response structures with JSONDecoder - swift

Got the following data model:
class ResponseMultipleElements<Element: Decodable>: Decodable {
let statuscode: Int
let response_type: Int
let errormessage: String?
let detailresponse: Element?
}
class Element<T: Decodable>: Decodable {
let count: String;
let element: T?
}
For the following API response structure:
{
"statuscode": 200,
"response_type": 3,
"errormessage": null,
"detailresponse": {
"count": "1",
"campaigns": [
{
"id": 1,
"name": "Foo",
"targetagegroup": null,
"creator":...
...
}
}
}
I'm triggering JSONDecoder like this:
class APIService: NSObject {
func getCampaignList(completion: #escaping(Result<[Campaign], APIError>) -> Void) {
guard let endpoint = URL(string: apiBaseUrlSecure + "/campaignlist") else {fatalError()}
var request = URLRequest(url: endpoint)
request.addValue("Bearer " + UserDefaults.standard.string(forKey: "authtoken")!, forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let jsonData = data
else { print("ERROR: ", error ?? "unknown error"); completion(.failure(.responseError)); return }
do {
let response = try JSONDecoder().decode(ResponseMultipleElements<[Campaign]>.self, from: jsonData)
completion(.success(response.detailresponse!))
} catch {
print("Error is: ", error)
completion(.failure(.decodingError))
}
}
dataTask.resume()
}
...
}
And I'm finally trying to make use of the decoded campaign object like this
class CoopOverviewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
//do stuff
// load Campaigns
self.apiService.getCampaignList(completion: {result in
switch result {
case .success(let campaigns):
DispatchQueue.main.async {
print("CAMPAIGN DATA: ", campaigns[0].name)
}
case .failure(let error):
print("An error occured \(error.localizedDescription)")
}
})
...
}
Now I've got 2 questions:
1)
let element: T?
is actually called "campaigns" in the api response for this call. However, it could be cooperations, payments, etc. in other api responses with that same ResponseMultipleElements surrounding structure. Is there a way to make the key swappable here, like I've done with the value with the use of generics? If not, how else would I solve that problem?
2) I'm getting this error:
typeMismatch(Swift.Array<Any>,
Swift.DecodingError.Context(codingPath:
[CodingKeys(stringValue: "detailresponse", intValue: nil)],
debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
I've told Swift that the "campaigns" part of the detailresponse is an Array of campaign objects - at least that's my understanding when looking at the api response. However, the error seems to say it's a dictionary. First, I don't get why that is and would really like to understand it. Second, I don't know how to tell it that it should expect a dictionary instead of an array then - getting confused with generics here a bit.
Thank you so much for your help in advance!

This is an approach to add a custom key decoding strategy to map any CodingKey but count in detailresponse to fixed value element.
First of all create a custom CodingKey
struct AnyCodingKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) {
return nil
}
}
Then create the structs similar to Sh_Khan's answer, in most cases classes are not needed
struct ResponseMultipleElements<T: Decodable>: Decodable {
let statuscode : Int
let response_type : Int
let errormessage : String?
let detailresponse : Element<T>
}
struct Element<U: Decodable>: Decodable {
let count : String
let element : U
}
struct Campaign : Decodable {
let id : Int
let name : String
let targetagegroup : String?
}
Now comes the funny part. Create a custom key decoding strategy which returns always element for the CodingKey in detailresponse which is not count
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom { codingKeys in
let lastKey = codingKeys.last!
if lastKey.intValue != nil || codingKeys.count != 2 { return lastKey }
if lastKey.stringValue == "count" { return lastKey }
return AnyCodingKey(stringValue: "element")!
}
let result = try decoder.decode(ResponseMultipleElements<[Campaign]>.self, from: data)
completion(.success(result.detailresponse.element))
} catch {
print("Error is: ", error)
completion(.failure(error))
}

Related

How to use a value from a struct dictionary and convert it to another type?

Here I want to be able to use the value returned from an array. It returns as a type from a struct. I'm unsure of how to use the value as an integer.
struct Item: Codable {
let data: [String : Datum]
}
struct Datum: Codable {
let value: Int
}
var array = Item(data: ["1" : Datum(value: 1),"2": Datum(value: 2), "3":Datum(value: 3)])
var keyArray = ["1", "2", "3"]
print(array.data[keyArray[0]]!)
// Prints Datum(value: 1)
print(array.data[keyArray[0]]! + 1)
//This produces an error "Cannot convert value of type 'Datum' to expected argument type 'Int'"
//Expected result should be 2
My use case is when I get returned a decoded JSON it normally comes back as a dictionary. I'm wanting to use the values returned with a key but I feel like I'm one step short.
Context
Full JSON Link
I'm going to retrieve values from this JSON. (Example from large JSON file)
{"data":{"2":{"high":179,"highTime":1628182107,"low":177,"lowTime":1628182102},"6":{"high":189987,"highTime":1628179815,"low":184107,"lowTime":1628182100},"8":{"high":190800,"highTime":1628181435,"low":188100,"lowTime":1628182095}
}}
The string in front refers to an item ID.
The struct that I came up to decode goes like this.
// MARK: - Single
struct Single: Codable {
let data: [String: Datum]
}
// MARK: - Datum
struct Datum: Codable {
let high, highTime: Int
let low, lowTime: Int?
}
From there I'm planning to iterate through the JSON response to retrieve the item prices I'd want.
#available(iOS 15.0, *)
struct ContentView: View {
#State var dataFromURL: Single = Single(data: [:])
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
async {
try await decode()
}
}
}
func decode() async throws -> Single {
let decoder = JSONDecoder()
let urlString = "https://prices.runescape.wiki/api/v1/osrs/latest"
guard let url = URL(string: urlString) else { throw APIError.invalidURL }
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw APIError.invalidServerResponse }
guard let result = try? decoder.decode(Single.self, from: data) else { throw APIError.invalidData }
//We copy our result to an existing variable
dataFromURL = result
return result
}
}
enum APIError: Error {
case invalidURL
case invalidServerResponse
case invalidData
}
extension APIError: CustomStringConvertible {
public var description: String {
switch self {
case.invalidURL:
return "Bad URL"
case .invalidServerResponse:
return "The server did not return 200"
case .invalidData:
return "Their server returned bad data"
}
}
}
I haven't gotten further than grabbing the response from the URL. That is why once I start manipulating the data I'd like to use the response to find other things like what would a profit/loss with another item become. Which isn't the goal of this question at the moment.
The object model to parse that JSON would be:
struct Price: Decodable {
let high: Int?
let highTime: Date?
let low: Int?
let lowTime: Date?
}
struct ResponseObject: Decodable {
let prices: [String: Price]
enum CodingKeys: String, CodingKey {
case prices = "data"
}
}
(Note, the documentation says that either high or low might be missing, so we have to make them all optionals.)
Now, the id number is being passed as a string in the JSON/ResponseObject. But that is a number (look at mapping). So, I would remap that dictionary so that the key was an integer, e.g.
enum ApiError: Error {
case unknownError(Data?, URLResponse?)
}
func fetchLatestPrices(completion: #escaping (Result<[Int: Price], Error>) -> Void) {
let url = URL(string: "https://prices.runescape.wiki/api/v1/osrs/latest")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? ApiError.unknownError(data, response)))
return
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
do {
let responseObject = try decoder.decode(ResponseObject.self, from: responseData)
let keysAndValues = responseObject.prices.map { (Int($0.key)!, $0.value) }
let prices = Dictionary(uniqueKeysWithValues: keysAndValues)
completion(.success(prices))
} catch {
completion(.failure(error))
}
}
task.resume()
}
The code that converts that [String: Price] to a [Int: Price] is this:
let keysAndValues = responseObject.prices.map { (Int($0.key)!, $0.value) }
let prices = Dictionary(uniqueKeysWithValues: keysAndValues)
I must say that this is a questionable API design, to have keys returned as integers in one endpoint and as strings as another. But it is what it is. So, the above is how you handle that.
Anyway, now that you have a dictionary of prices, keyed by the id numbers, you can use that in your code, e.g.
var prices: [Int: Price] = [:]
var products: [Product] = []
let group = DispatchGroup()
group.enter()
fetchLatestPrices { result in
defer { group.leave() }
switch result {
case .failure(let error):
print(error)
case .success(let values):
prices = values
}
}
group.enter()
fetchProducts { result in
defer { group.leave() }
switch result {
case .failure(let error):
print(error)
case .success(let values):
products = values }
}
group.notify(queue: .main) {
for product in products {
print(product.name, prices[product.id] ?? "no price found")
}
}
Where
func fetchProducts(completion: #escaping (Result<[Product], Error>) -> Void) {
let url = URL(string: "https://prices.runescape.wiki/api/v1/osrs/mapping")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? ApiError.unknownError(data, response)))
return
}
do {
let products = try JSONDecoder().decode([Product].self, from: responseData)
completion(.success(products))
} catch {
completion(.failure(error))
}
}
task.resume()
}
And
struct Product: Decodable {
let id: Int
let name: String
let examine: String
let members: Bool
let lowalch: Int?
let limit: Int?
let value: Int
let highalch: Int?
let icon: String
}
(As an aside, I do not know if some of these other properties should be optionals or not. I just used optionals where I empirically discovered that they are occasionally missing.)

Parsing JSON to a struct with SwiftyJSON

I have a simple struct to handle the data parsing from the SwiftyJSON
struct Threads{
var threads:[ThreadDetail]
}
struct ThreadDetail {
var title:String
var username:String
var prefix_id:Int
}
Here's a sample of the API response
{
"threads": [
{
"first_post_id": 258535,
"prefix_id": 1,
"thread_id": 50204,
"title": "Testing board title",
"user_id": 20959,
"username": "test",
"view_count": 247,
}
Now here's the part where I couldn't figure out how
Alamofire.request(url, method: .get, headers: headers).validate().responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
for item in json["threads"].arrayValue {
//how should it be written here?
}
case .failure(let error):
print(error)
}
}
}
}
Declare your models and conform them Codable.
struct Response: Codable {
let threads: [Thread]
}
// MARK: - Thread
struct Thread: Codable {
let firstPostID, prefixID, threadID: Int
let title: String
let userID: Int
let username: String
let viewCount: Int
enum CodingKeys: String, CodingKey {
case firstPostID = "first_post_id"
case prefixID = "prefix_id"
case threadID = "thread_id"
case title
case userID = "user_id"
case username
case viewCount = "view_count"
}
}
After that, convert your data to model by using JSONDecoder
Alamofire.request(urlString).response {
response in
guard let data = response.data else { return }
do {
let decoder = JSONDecoder()
let threadsWrapper = try decoder.decode(Response.self, from: data)
} catch let error {
print(error)
}
Use this website to convert your JSON to Codable
https://app.quicktype.io/
You can do it by following as far as I remember SwiftyJSON:
Alamofire.request(url, method: .get, headers: headers).validate().responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
var models = [ThreadDetail]()
for item in json["threads"].arrayValue {
let model = ThreadDetail(title: item["title"].stringValue,
username: item["username"].stringValue,
prefix_id: item["prefix_id"].intValue)
models.append(model)
}
// do whatever you need with models
case .failure(let error):
print(error)
}
}
But really, as like Frankenstein said, the best way to solve your problem is just to conform your models to Codable.
I personally use a library I custom made for apps that use JSON called Weave. It's built on top of NSURLSession and fully native Swift. When you create a request using WVRequest.request(), for the argument outputType, use the type .json.
Here's your above code using Weave:
WVRequest.start(url, requestType: .get, outputType:.json, headers: headers).start() { response in
if response.success {
// Cast to a dictionary
let json = (response as! WVJSONResponse).json as! [String:Any]
let threads = json["threads"] as! [Thread]
// From here, just use a for loop, and create an instance of your Thread class for each iteration.
} else {
print("Error!")
}
}
Hope that helps!
struct Temperature: Codable {
let high: Double
let low: Double
}
struct Bluh: Codable, Identifiable {
var temperature: JSON
func getTemperatures() -> Temperature? {
do {
let decoder = JSONDecoder()
return try decoder.decode(Temperature.self, from: temperature.rawData())
} catch let error {
print(error)
}
}
}
You can do sth like tis too. If your data is SWIFY JSON and trying to convert to struct.

Passing value from Alamofire get request to variable

Here is my code that parse data but I want to get jsonDictionary and assign it to another variable for using outer scope.
I've checked many answers but almost all are use print for showig outcome , I need to get value instead of showing it in console.
Can anyone help me please to solve it
public func fetchData() {
let request = Alamofire.request("www.anyurl.com")
request.responseJSON { data in
if let json = data.result.value as? [String: Any] {
guard let jsonArray = json["data"] as? [[String: Any]] else {
return
}
if let jsonDictionary = jsonArray[0]["title"] as? [[String:Any]] {
print(jsonDictionary)
}
}
}
}
As a example we get this simple Json Response.
{
"success": true,
"Message": "data retrieved successfully",
"data": {
"name": "Dilan",
"age": "26",
"country": "Sri Lanka"
}
}
simply you can define public variable in somewhere in your project like
public static var user:NSDictionary = [:]
Then you can directly assign your json response as a dictionary to that variable and use it in your project.
AF.request(URL(string: "someurl")!,
method: .get,
headers:AppConstants.headers)
.validate(statusCode:200..<300)
.validate(contentType:[AppConstants.contentTypeJson])
.responseJSON { (response) in
switch response.result{
case .success(let data):
guard let json = data as? [String:AnyObject] else {
return
}
let isSuccess = (json["success"])?.boolValue ?? false
if isSuccess {
SomeClass.user = json["data"] as? NSDictionary ?? [:]
}else{
print("false")
}
case .failure(let error):
print("Error")
}
}
This is the another way to do that using Codable,Decodable
first create Codable Struct
import Foundation
// MARK: - User
struct User: Codable {
let success: Bool
let message: String
let data: DataClass
enum CodingKeys: String, CodingKey {
case success
case message = "Message"
case data
}
}
// MARK: - DataClass
struct DataClass: Codable {
let name, age, country: String
}
then you can convert your response directly to codable object
AF.request(URL(string: "someurl")!,
method: .get,
headers:AppConstants.headers)
.validate(statusCode:200..<300)
.validate(contentType:[AppConstants.contentTypeJson])
.responseJSON { (response) in
switch response.result{
case .success(let data):
let decoder = JSONDecoder()
do {
SomeClass.User= try decoder.decode(User.self, from: data)
//this time user should be public static var user:User?
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print("Error")
}
}

Decoding dynamic JSON structure in swift 4

I have the following issue that I'm not sure how to handle.
My JSON response can look like this:
{
"data": {
"id": 7,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDY1MTU0NDMsImRhdGEiOiJ2bGFkVGVzdCIsImlhdCI6MTU0NjUwODI0M30.uwuPhlnchgBG4E8IvHvK4bB1Yj-TNDgmi7wUAiKmoVo"
},
"error": null
}
Or like this:
{
"data": [{
"id": 12
}, {
"id": 2
}, {
"id": 5
}, {
"id": 7
}],
"error": null
}
So in short the data can be either a single objet or an Array. What i have is this:
struct ApiData: Decodable {
var data: DataObject?
var error: String?
}
struct DataObject: Decodable {
var userId: Int?
enum CodingKeys: String, CodingKey {
case userId = "id"
}
}
This works fine for the first use case, but it will fail once data turns into
var data: [DataObject?]
How do I make that dynamic without duplicating code?
Edit: This is how i decode the object as well
func makeDataTaskWith(with urlRequest: URLRequest, completion: #escaping(_ apiData: ApiData?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: urlRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
if let responseCode = response as? HTTPURLResponse {
print("Response has status code: \(responseCode.statusCode)")
}
do {
let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(retreived)
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
}
If data can be a single object or an array write a custom initializer which decodes first an array, if a type mismatch error occurs decode a single object. data is declared as an array anyway.
As token appears only in a single object the property is declared as optional.
struct ApiData: Decodable {
let data : [DataObject]
let error : String?
private enum CodingKeys : String, CodingKey { case data, error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
data = try container.decode([DataObject].self, forKey: .data)
} catch DecodingError.typeMismatch {
data = [try container.decode(DataObject.self, forKey: .data)]
}
error = try container.decodeIfPresent(String.self, forKey: .error)
}
}
struct DataObject: Decodable {
let userId : Int
let token : String?
private enum CodingKeys: String, CodingKey { case userId = "id", token }
}
Edit: Your code to receive the data can be improved. You should add a better error handling to return also all possible errors:
func makeDataTaskWith(with urlRequest: URLRequest, completion: #escaping(ApiData?, Error?) -> Void) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: urlRequest) {
(data, response, error) in
if let error = error { completion(nil, error); return }
if let responseCode = response as? HTTPURLResponse {
print("Response has status code: \(responseCode.statusCode)")
}
do {
let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data!)
completion(retreived, nil)
} catch {
print("Decoder error: ", error)
completion(nil, error)
}
}.resume()
}
Using power of generic, it simple like below:
struct ApiData<T: Decodable>: Decodable {
var data: T?
var error: String?
}
struct DataObject: Decodable {
private var id: Int?
var userId:Int? {
return id
}
}
Use
if let obj = try? NetworkManager.shared.decoder.decode(ApiData<DataObject>.self, from: data) {
//Do somthing
} else if let array = try NetworkManager.shared.decoder.decode(ApiData<[DataObject]>.self, from: data) {
// Do somthing
}
If you have only two possible outcomes for your data, an option would be to try and parse data to one of the expected types, if that fails you know that the data is of other type and you can then handle it accordingly.
See this
You can try
struct Root: Codable {
let data: DataUnion
let error: String?
}
enum DataUnion: Codable {
case dataClass(DataClass)
case datumArray([Datum])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([Datum].self) {
self = .datumArray(x)
return
}
if let x = try? container.decode(DataClass.self) {
self = .dataClass(x)
return
}
throw DecodingError.typeMismatch(DataUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for DataUnion"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .dataClass(let x):
try container.encode(x)
case .datumArray(let x):
try container.encode(x)
}
}
}
struct Datum: Codable {
let id: Int
}
struct DataClass: Codable {
let id: Int
let token: String
}
let res = try? JSONDecoder().decode(Root.self, from:data)

Swift 4 Codable - API provides sometimes an Int sometimes a String

I have Codables running now. But the API has some String entries that can sometimes have an Int value of 0 if they are empty. I was searching here and found this: Swift 4 Codable - Bool or String values But I'm not able to get it running
My struct
struct check : Codable {
let test : Int
let rating : String?
}
Rating is most of the time something like "1Star". But if there is no rating I get 0 as Int back.
I'm parsing the data like this:
enum Result<Value> {
case success(Value)
case failure(Error)
}
func checkStar(for userId: Int, completion: ((Result<check>) -> Void)?) {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "xyz.com"
urlComponents.path = "/api/stars"
let userIdItem = URLQueryItem(name: "userId", value: "\(userId)")
urlComponents.queryItems = [userIdItem]
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Authorization": "Bearer \(keytoken)"
]
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
// Now we have jsonData, Data representation of the JSON returned to us
// from our URLRequest...
// Create an instance of JSONDecoder to decode the JSON data to our
// Codable struct
let decoder = JSONDecoder()
do {
// We would use Post.self for JSON representing a single Post
// object, and [Post].self for JSON representing an array of
// Post objects
let posts = try decoder.decode(check.self, from: jsonData)
completion?(.success(posts))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
Loading it:
func loadStars() {
checkStar(for: 1) { (result) in
switch result {
case .success(let goo):
dump(goo)
case .failure(let error):
fatalError(error.localizedDescription)
}
}
}
I hope someone can help me there, cause I'm not completely sure how this parsing, etc. works.
you may implement your own decode init method, get each class property from decode container, during this section, make your logic dealing with wether "rating" is an Int or String, sign all required class properties at last.
here is a simple demo i made:
class Demo: Decodable {
var test = 0
var rating: String?
enum CodingKeys: String, CodingKey {
case test
case rating
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let test = try container.decode(Int.self, forKey: .test)
let ratingString = try? container.decode(String.self, forKey: .rating)
let ratingInt = try? container.decode(Int.self, forKey: .rating)
self.rating = ratingString ?? (ratingInt == 0 ? "rating is nil or 0" : "rating is integer but not 0")
self.test = test
}
}
let jsonDecoder = JSONDecoder()
let result = try! jsonDecoder.decode(Demo.self, from: YOUR-JSON-DATA)
if rating API's value is normal string, you will get it as you wish.
if rating API's value is 0, rating will equal to "rating is nil or 0"
if rating API's value is other integers, rating will be "rating is integer but not 0"
you may modify decoded "rating" result, that should be easy.
hope this could give you a little help. :)
for more info: Apple's encoding and decoding doc