Convenient way to decode nested JSON object in Swift? - json

Let's say you have some JSON:
{
"status": "error",
"data": {
"errormessage": "Could not get user with ID: -1.",
"errorcode": 14
}
}
For a given Error struct:
struct APIError: Decodable {
let code: Int?
let message: String?
enum CodingKeys: String, CodingKey {
case code = "errorcode"
case message = "errormessage"
}
}
Hit the web service, get the JSON, and initialize the struct:
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest)
{ (data, response, error) in
// Doesn't work because the portion of the JSON we want is in the "data" key
let e = try? JSONDecoder().decode(APIError.self, from: data)
}
task.resume()
Is there some easy way to do something like data["data"]? What's the correct model to follow?
Solution A - Convert the data to a JSON object, get the object we want, then convert it to a Data object and decode.
let jsonFull = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
let json = jsonFull["data"]
let data_error = try? JSONSerialization.data(withJSONObject: json, options: [])
let e = try? JSONDecoder().decode(APIError.self, from: data_error)
Solution B - Wrap the target item in another struct
struct temp : Decodable {
let status: String?
let data: APIError?
}
let e = try? JSONDecoder().decode(temp.self, from: data).data
Solution C - Set the nested structure in decode (what if it is several objects deep?)
let e = try? JSONDecoder().decode([Any, APIError.self], from: data)
What patterns am I missing? What's the most elegant way to do this?

You can use the following approach:
struct APIError: Decodable {
let code: Int
let message: String
enum CodingKeys: String, CodingKey {
case data
}
enum ErrorCodingKeys: String, CodingKey {
case code = "errorcode"
case message = "errormessage"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let nestedContainer = try container.nestedContainer(keyedBy: ErrorCodingKeys.self, forKey: .data)
code = try nestedContainer.decode(Int.self, forKey: .code)
message = try nestedContainer.decode(String.self, forKey: .message)
}
}
let data = try! JSONSerialization.data(withJSONObject: ["status": "error", "data": ["errorcode": 14, "errormessage": "Could not get user with ID: -1."]], options: [])
let error = try! JSONDecoder().decode(APIError.self, from: data)

Related

Translate a json dictionary object to Array of objects

I am getting back JSON that looks like this:
{
"success":true,
"timestamp":1650287883,
"base":"EUR",
"date":"2022-04-18",
"rates":{
"USD":1.080065,
"EUR":1,
"JPY":136.717309,
"GBP":0.828707,
"AUD":1.465437,
"CAD":1.363857
}
}
I was expecting rates to be an array, but it's an object. The currency codes may vary. Is there a way to Decode this with Swift's built-in tools?
I'm certain this won't work:
struct ExchangeRateResponse: Decodable {
let success: Bool
let base: String
let date: Date
let rates: [[String: Double]]
private enum ResponseKey: String, CodingKey {
case success, base, date, rates
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResponseKey.self)
success = try container.decode(Bool.self, forKey: .success)
base = try container.decode(String.self, forKey: .base)
date = try container.decode(Date.self, forKey: .date)
rates = try container.decode([[String: Double]].self, forKey: .rates)
}
}
your model is wrong. you can do this:
struct YourModelName {
let success: Bool
let timestamp: Int
let base, date: String
let rates: [String: Double]
}
after that you can try do decode it.
something like this:
do {
let jsonDecoder = JSONDecoder()
let loadData = try jsonDecoder.decode(YourModelName.self, from: data!)
// 'loadData' is your data that you want. for your problem you have to use 'loadData.rates'. Hint: you have to use it in 'for' loop!
DispatchQueue.main.async { _ in
// if you have to update your UI
}
} catch {
print(error)
}
First of all you cannot decode date to Date out of the box, but you can decode timestamp to Date.
Second of all it's impossible to decode a dictionary to an array. This is like apples and oranges.
But fortunately you can map the dictionary to an array because it behaves like an array (of tuples) when being mapped.
Just create an other struct Rate
struct Rate {
let code: String
let value: Double
}
struct ExchangeRateResponse: Decodable {
let success: Bool
let timestamp: Date
let base: String
let date: String
let rates: [Rate]
private enum CodingKeys: String, CodingKey {
case success, base, timestamp, date, rates
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
success = try container.decode(Bool.self, forKey: .success)
timestamp = try container.decode(Date.self, forKey: .timestamp)
base = try container.decode(String.self, forKey: .base)
date = try container.decode(String.self, forKey: .date)
let rateData = try container.decode([String: Double].self, forKey: .rates)
rates = rateData.map(Rate.init)
}
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let result = try decoder.decode(ExchangeRateResponse.self, from: data)
print(result)
} catch {
print(error)
}
Or still shorter if you map the dictionary after decoding the stuff
struct ExchangeRateResponse: Decodable {
let success: Bool
let timestamp: Date
let base: String
let date: String
let rates: [String:Double]
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let result = try decoder.decode(ExchangeRateResponse.self, from: data)
let rates = result.rates.map(Rate.init)
print(rates)
} catch {
print(error)
}

Converting API JSON data to a Swift struct

I am using Swift for the first time and I'd like to be able to process some info from an API response into a usable Swift object.
I have (for example) the following data coming back from my API:
{
data: [{
id: 1,
name: "Fred",
info: {
faveColor: "red",
faveShow: "Game of Thrones",
faveIceCream: "Chocolate",
faveSport: "Hockey",
},
age: "28",
location: "The Moon",
},{
...
}]
}
In swift I have the data coming back from the API. I get the first object and I'm converting it and accessing it like so:
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let dataParentNode = json["data"] as! [[String:Any]]
let firstObject = dataParentNode[0]
let _id = firstObject["id"] as? String ?? "0"
let _name = firstObject["name"] as? String ?? "Unknown"
This is fine until I want to start processing the sub-objects belonging to the first object so I came up with the following structs to try and make this cleaner.
Please note - I don't need to process all of the JSON data coming back so I want to convert it to what I need in the structs
struct PersonInfo : Codable {
let faveColor: String?
let faveShow: String?
}
struct Person : Codable {
let id: String?
let name: String?
let info: PersonInfo?
}
When I take this:
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let dataParentNode = json["data"] as! [[String:Any]]
let firstObject = dataParentNode[0]
and then try to convert firstObject to Person or firstObject["info"] to PersonInfo I can't seem to get it to work (I get nil).
let personInfo = firstObject["info"] as? PersonInfo
Can anyone advise please? I just need to get my head around taking API response data and mapping it to a given struct (with sub-objects) ignoring the keys I don't need.
You can simply use decode(_:from:) function of JSONDecoder for this:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([String: [Person]].self, from: data)
let firstObject = decoded["data"]?.first
} catch {
print(error)
}
Even better you can add another struct to you model like this:
struct PersonsData: Codable {
let data: [Person]
}
And map your JSON using that type:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(PersonsData.self, from: data)
let firstObject = decoded.data.first
} catch {
print(error)
}
Update: Your Person struct might need a little change, because the id property is integer in your JSON.
So, it will end up like this:
struct Person : Codable {
let id: Int?
let name: String?
let info: PersonInfo?
}

Alamofire JSON Response Error with post parameter

I use alamofire and swityjson, although I use it in the same way, I did not get any results here.
let exampleURl = URL(string: exampleUrl)!
let params: [String: String] = ["id": "expampleString"]
let headers: HTTPHeaders = [
"charset": "UTF-8",
"Accept": "application/json"
]
Alamofire.request(exampleURL, method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).validate(statusCode: 200..<600).responseJSON() { response
in
switch response.result {
case.success:
if let json = response.data {
do{
let data = try JSON(data: json)
let str = data
print(str["arrayName"])
let arrayData = str["arrayName"].arrayValue.map{$0["content"].stringValue}
print(arrayData[0])
let credit = arrayData[0]
}
catch{
print("JSON Error")
}
}
case .failure(let error):
print("RESPONSE ERROR: \(error)")
}
}
This is my Json output.
{"arrayName":[{"content":"Hello_World"}]}
This is Error. I don't understand. I send post parameters but i can't fetch parameter in Json array.
RESPONSE ERROR: responseSerializationFailed(reason: Alamofire.AFError.ResponseSerializationFailureReason.jsonSerializationFailed(error: Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around character 0." UserInfo={NSDebugDescription=Invalid value around character 0.}))
it seems like u used alamofire in the wrong way, try it out please:
here is the request:
let url = URL(string: "YOUR LINK HERE")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
let jsonDecoder = JSONDecoder()
let responseModel = try jsonDecoder.decode(BaseModel.self, from: data!)
}
task.resume()
here is your swift model classes:
import Foundation
struct ArrayName : Codable {
let content : String?
enum CodingKeys: String, CodingKey {
case content = "content"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
content = try values.decodeIfPresent(String.self, forKey: .content)
}
}
struct BaseModel : Codable {
let arrayName : [ArrayName]?
enum CodingKeys: String, CodingKey {
case arrayName = "arrayName"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
arrayName = try values.decodeIfPresent([ArrayName].self, forKey: .arrayName)
}
}
That error usually indicates you're not getting a JSON response. You need to debug that response, usually by printing it as a String or setting a breakpoint in the response handler and inspecting it there.

decoding a json to generic array or class in swift

How do you decode json to a generic model in swift?
In java for decoding json I use GSON and in general it does not matter I use <T<E>> or ArrayList<E>.In swift Array is a struct and can't be inheritance and it has not implemented Decodable.
I'm looking for a generic elegant class to use in all my web service.
My scenario:
I have json response
{
"status": true,
"message": "",
"code": 200,
"response": [{
"id": 43
}]
}
and a generic reponse model like this from web services:
class GeneralResponse< T : Decodable >:NSObject,Decodable{
var status = false
var message = ""
var code = -1
var response : T?
private enum CodingKeys: String, CodingKey {
case status
case message
case code
case response
}
required public init(from decoder: Decoder) throws{
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(Bool.self, forKey: .status)
message = try container.decode(String.self, forKey: .message)
code = try container.decode(Int.self, forKey: .code)
response = try container.decode(T.self, forKey: .response)
}
}
class ItemDemoModel:Decodable {
var id = -1
private enum ItemDemModelCodingKeys : String, CodingKey {
case id
}
required init(from decoder:Decoder) throws {
let container = try decoder.container(keyedBy: ItemDemModelCodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
}
}
response variable can be ItemDemoModel or an array of ItemDemoModel.
For example:
It can be GeneralResponse<Array<ItemDemoModel>>>
or GeneralResponse<ItemDemoModel>>
thanks.
If you declare a Decodable properties with same name as the key in json then you don't really need an enum to define Coding keys and an initializer to manually map every property with the key.
Also, there is no need to inherit from NSObject in Swift until you have a specific use case for that. Looking at the declaration, it seems unnecessary so your GeneralResponse can be redeclared as simple as this,
class GeneralResponse<T: Decodable>: Decodable {
var code: Int
var status: Bool
var message: String?
var response : T?
}
Similarly, ItemDemoModel can be declared as this,
class ItemDemoModel: Decodable {
var id: Int
}
Now you can setup your service as below to get the GeneralResponse<T> for any request,
struct RequestObject {
var method: String
var path: String
var params: [String: Any]
}
class WebService {
private let decoder: JSONDecoder
public init(_ decoder: JSONDecoder = JSONDecoder()) {
self.decoder = decoder
}
public func decoded<T: Decodable>(_ objectType: T.Type,
with request: RequestObject,
completion: #escaping (GeneralResponse<T>?, Error?) -> Void) {
// Here you should get data from the network call.
// For compilation, we can create an empty object.
let data = Data()
// Now parsing
do {
let response = try self.decoder.decode(GeneralResponse<T>.self, from: data)
completion(response, nil)
} catch {
completion(nil, error)
}
}
}
Usage
let request = RequestObject(method: "GET", path: "https://url.com", params: [:])
WebService().decoded([ItemDemoModel].self, with: request) { (response, error) in
if let items = response?.response {
print(items)
}
}
P.S; You must be used to declare arrays and dictionaries as below,
let array: Array<SomeType>
let dictionary: Dictionary<String: SomeType>
let arrayOfDictionary: Array<Dictionary<String: SomeType>>
But with Swift's type inference, you can declare an array and a dictionary as simple as below,
let array: [SomeType]
let dictionary: [String: SomeType]
let arrayOfDictionary: [[String: SomeType]]
Here you have a function you may want to use in order to decode your JSON:
func decode<T: Decodable>(_ data: Data, completion: #escaping ((T) -> Void)) {
do {
let model = try JSONDecoder().decode(T.self, from: data)
completion(model)
} catch {
log(error.localizedDescription, level: .error)
}
}
So you can just call your function like:
decode(data, completion: { (user: User) in
// Do something with your parsed user struct or whatever you wanna parse
})
I hope this helps :D
Array<T> conforms to Decodable if T conforms to Decodable, so GeneralResponse<[ItemDemoModel]> won't produce any errors.
As shown here:
You can simply do this:
let decoder = JSONDecoder()
let obj = try decoder.decode(type, from: json.data(using: .utf8)!)

Swift Decoding nested JSON

I have a problem with parsing data from NBP api "http://api.nbp.pl/api/exchangerates/tables/a/?format=json" . I created struct CurrencyDataStore and Currency
struct CurrencyDataStore: Codable {
var table: String
var no : String
var rates: [Currency]
enum CodingKeys: String, CodingKey {
case table
case no
case rates
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
table = ((try values.decodeIfPresent(String.self, forKey: .table)))!
no = (try values.decodeIfPresent(String.self, forKey: .no))!
rates = (try values.decodeIfPresent([Currency].self, forKey: .rates))!
} }
struct Currency: Codable {
var currency: String
var code: String
var mid: Double
enum CodingKeys: String, CodingKey {
case currency
case code
case mid
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
currency = try values.decode(String.self, forKey: .currency)
code = try values.decode(String.self, forKey: .code)
mid = try values.decode(Double.self, forKey: .mid)
}
}
In controllerView class I wrote 2 methods to parse data from API
func getLatestRates(){
guard let currencyUrl = URL(string: nbpApiUrl) else {
return
}
let request = URLRequest(url: currencyUrl)
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if let error = error {
print(error)
return
}
if let data = data {
self.currencies = self.parseJsonData(data: data)
}
})
task.resume()
}
func parseJsonData(data: Data) -> [Currency] {
let decoder = JSONDecoder()
do{
let currencies = try decoder.decode([String:CurrencyDataStore].self, from: data)
}
catch {
print(error)
}
return currencies
}
This code didn't work. I have this error "typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil))".
Could you help me?
The JSON being returned by that API gives you an array, not a dictionary, but you're telling the JSONDecoder to expect a dictionary type. Change that line to:
let currencies = try decoder.decode([CurrencyDataStore].self, from: data)