How can I read the nested json data by using swift combine - json

I just started using combine swift to handle the data request and response.
The Json data returned is a nested data which I will only need one part of it.
such as:
{
"page": 1,
"data": [
{
"id": 1,
"title": "news-1",
"content": "content 1"
},
{
"id": 2,
"title": "news-2",
"content": "content 2"
},
{
"id": 3,
"title": "news-3",
"content": "content 3"
}
],
"time": 202021313,
"description" :"xxxx"
}
I will need to use the data array.
Fetch functions below:
func fetchData() throws -> URLSession.DataTaskPublisher {
let headers = [
"Content-Type": "application/json",
"cache-control": "no-cache",
]
guard let url = URL(string: endpointStr ) else {
throw APIError.invalidEndpoint
}
var request = URLRequest(url: url,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
return session.dataTaskPublisher(for: request)
}
let publisher = try? fetchData()
let decoder = JSONDecoder()
let cancellable = publisher?
.receive(on: DispatchQueue.main)
.map {
$0.data
}
.decode(type: DataModel.self, decoder: decoder)
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure(let error):
print("Error:")
print(error)
case .finished:
print("DONE - get Publisher")
}
}, receiveValue: { data in
print(data.title)
})
The data it returned is the complete json data, is there any elegant way to get only the array of data and convert into an array of [DataModel] and handle the data in receiveValue.
I have tried to edit map with no luck:
.map {
if let dataString = String(data: $0.data, encoding: .utf8) {
let dataDic = APIService.convertToDictionary(text: dataString)
if let DataArray = dataDic?["data"] {
return listDataDic!
}
return $0.data
}

Please clarify if i've misunderstood the question but what if you use another model to decode your [DataModel] and then map to the decoded [DataModel] array?
Here is a unit test example. Response is the new model that decodes the [DataModel] array into something you can work with.
import XCTest
import Combine
let data = """
{
"page": 1,
"data": [
{
"id": 1,
"title": "news-1",
"content": "content 1"
},
{
"id": 2,
"title": "news-2",
"content": "content 2"
},
{
"id": 3,
"title": "news-3",
"content": "content 3"
}
],
"time": 202021313,
"description": "xxxx"
}
""".data(using: .utf8)!
class Response: Codable {
var data: [DataModel]
}
class DataModel: Codable {
let id: Int
let title: String
let content: String
}
class Test: XCTestCase {
func testDecodeDataModel() {
let e = expectation(description: "finished expectation")
let decoder = JSONDecoder()
let cancellable = Just(data)
.decode(type: Response.self, decoder: decoder)
.map { $0.data }
.sink(receiveCompletion: { (completion) in
// handle completion..
}, receiveValue: { dataArray in
print(dataArray.count) // here you can work with your [DataModel] array
e.fulfill()
})
wait(for: [e], timeout: 1)
}
}

Related

Decode different incoming JSON types in Swift

I'm using JSONDecoder to decode incoming websocket messages from an API. Messages come from the websockettask as a String. Right now I have my Codable struct as such:
struct JsonRPCMessage: Codable {
let jsonrpc: String
let result: String?
let method: String?
let id: Int?
}
Then I just decode it like:
let message = try decoder.decode(JsonRPCMessage.self, from: data!)
This has worked fine for about half of the endpoints in the API which just return a single String for result. The others return a dictionary. When I change the type of result to Dictionary, the struct no longer conforms to Codable. When it's left as a string, the decoder returns a type mismatch error at runtime. Plus, changing the type to dictionary would break functionality for the rest of the api's features.
Looking for ideas to decode and access the string to value pairs in that dictionary as well as check for dictionary or string before sending it to the decoder.
Here are some samples of the different types of response I need to be able to sort and parse:
{
"jsonrpc": "2.0",
"result": {
"klippy_connected": true,
"klippy_state": "ready",
"components": [
"klippy_connection",
"history",
"octoprint_compat",
"update_manager"
],
"failed_components": [],
"registered_directories": [
"config",
"logs",
"gcodes",
"config_examples",
"docs"
],
"warnings": [],
"websocket_count": 4,
"moonraker_version": "v0.7.1-659-gf047167",
"missing_klippy_requirements": [],
"api_version": [1, 0, 5],
"api_version_string": "1.0.5"
},
"id": 50
}
{
"jsonrpc": "2.0",
"method": "notify_proc_stat_update",
"params": [
{
"moonraker_stats": {
"time": 1663016434.5099802,
"cpu_usage": 0.74,
"memory": 35716,
"mem_units": "kB"
},
"cpu_temp": null,
"network": {
"lo": { "rx_bytes": 2568, "tx_bytes": 2568, "bandwidth": 0.0 },
"tunl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
"ip6tnl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
"eth0": {
"rx_bytes": 2529302,
"tx_bytes": 13891023,
"bandwidth": 7005.14
}
},
"system_cpu_usage": {
"cpu": 25.62,
"cpu0": 1.98,
"cpu1": 1.0,
"cpu2": 0.0,
"cpu3": 100.0
},
"system_memory": {
"total": 8039920,
"available": 7182640,
"used": 857280
},
"websocket_connections": 4
}
]
}
{
"jsonrpc": "2.0",
"result": "ok",
"id": 50
}
In this case, the better option is to receive the same JSON in each case, but if you can't control that then you can implement custom decoding using init(from:).
struct JsonRPCMessage: Decodable {
enum CodingKeys: String, CodingKey {
case jsonrpc, result, method, id
}
let jsonrpc: String
let result: String?
let method: String?
let id: Int?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.jsonrpc = try container.decode(String.self, forKey: .jsonrpc)
if let dic = try container.decodeIfPresent([String: Any].self, forKey: .result) {
self.result = dic["YourKey"] as? String
}
else {
self.result = try container.decodeIfPresent(String.self, forKey: .result)
}
// If you have a custom type for Result than
if let result = try container.decodeIfPresent(YourResultType.self, forKey: .result) {
self.result = result.propertyOfYourResult
}
else {
self.result = try container.decodeIfPresent(String.self, forKey: .result)
}
self.method = try container.decodeIfPresent(String.self, forKey: .method)
self.id = try container.decodeIfPresent(Int.self, forKey: .id)
}
}
You're trying to force 3 different JSON shapes into the same Swift struct. This is generally not advised. You could do custom decoding as #Nirav suggested, but then you're essentially adding business logic decoding to your models and this will quickly grow out of hand and be untestable.
I believe a far better solution is to create 3 different struct and try to decode one, if not, try decoding the other, etc... and handle any error as appropriate, and test this behaviour:
import Foundation
import SwiftUI
let json1 = """
{
"jsonrpc": "2.0",
"result": {
"klippy_connected": true,
"klippy_state": "ready",
"components": [
"klippy_connection",
"history",
"octoprint_compat",
"update_manager"
],
"failed_components": [],
"registered_directories": [
"config",
"logs",
"gcodes",
"config_examples",
"docs"
],
"warnings": [],
"websocket_count": 4,
"moonraker_version": "v0.7.1-659-gf047167",
"missing_klippy_requirements": [],
"api_version": [1, 0, 5],
"api_version_string": "1.0.5"
},
"id": 50
}
""".data(using: .utf8)!
struct JSON1: Codable {
var jsonrpc: String
var result: JSONResult
var id: Int
struct JSONResult: Codable {
var klippy_connected: Bool
var klippy_state: String
var components: [String]
var failed_components: [String]
var registered_directories: [String]
// etc...
}
}
let json2 = """
{
"jsonrpc": "2.0",
"method": "notify_proc_stat_update",
"params": [
{
"moonraker_stats": {
"time": 1663016434.5099802,
"cpu_usage": 0.74,
"memory": 35716,
"mem_units": "kB"
},
"cpu_temp": null,
"network": {
"lo": { "rx_bytes": 2568, "tx_bytes": 2568, "bandwidth": 0.0 },
"tunl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
"ip6tnl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
"eth0": {
"rx_bytes": 2529302,
"tx_bytes": 13891023,
"bandwidth": 7005.14
}
},
"system_cpu_usage": {
"cpu": 25.62,
"cpu0": 1.98,
"cpu1": 1.0,
"cpu2": 0.0,
"cpu3": 100.0
},
"system_memory": {
"total": 8039920,
"available": 7182640,
"used": 857280
},
"websocket_connections": 4
}
]
}
""".data(using: .utf8)!
struct JSON2: Codable {
var jsonrpc: String
var params: [JSONParams]
var method: String
struct JSONParams: Codable {
var moonraker_stats: MoonrakerStats
// etc...
struct MoonrakerStats: Codable {
var time: Double
var cpu_usage: Double
// etc...
}
}
}
let json3 = """
{
"jsonrpc": "2.0",
"result": "ok",
"id": 50
}
""".data(using: .utf8)!
struct JSON3: Codable {
var jsonrpc: String
var result: String
var id: Int
}
let data = [json1, json2, json3].randomElement()!
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(JSON1.self, from: data) {
print("we have json 1")
print(decoded)
} else if let decoded = try? decoder.decode(JSON2.self, from: data) {
print("we have json 2")
print(decoded)
} else if let decoded = try? decoder.decode(JSON3.self, from: data) {
print("we have json 3")
print(decoded)
} else {
print("we don't know what we have")
}

Decode JSON Array with no Attribute Name

I have looked through other threads regarding trying to parse JSON data where a JSON array has no name. From what I have found you need to use a unkeyedContainer but I'm not entirely sure from the examples I have seen how this works with the data model.
Below is a snippet of data from open charge api:
[
{
"IsRecentlyVerified": false,
"ID": 136888,
"UUID": "254B0B07-E7FC-4B4B-A37C-899BCB9D7261",
"DataProviderID": 18,
"DataProvidersReference": "0a9fdbb17feb6ccb7ec405cfb85222c4",
"OperatorID": 3,
"UsageTypeID": 1,
"AddressInfo": {
"ID": 137234,
"Title": "Ballee Road Park & Share",
"AddressLine1": "Ballee Road",
"Town": "Ballymena",
"Postcode": "BT42 2HD",
"CountryID": 1,
"Latitude": 54.844648,
"Longitude": -6.273606,
"AccessComments": "Ballee Road Park and Share, Ballymena",
"RelatedURL": "http://pod-point.com",
"Distance": 3.81818421833416,
"DistanceUnit": 2
},
"Connections": [
{
"ID": 191571,
"ConnectionTypeID": 25,
"Reference": "1",
"StatusTypeID": 50,
"LevelID": 2,
"Amps": 32,
"Voltage": 400,
"PowerKW": 22,
"CurrentTypeID": 20
},
It looks to me that the first [ and { have no attribute names which I belive is creating the error in xcode: "Error!: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))"
Here is my data model:
import Foundation
struct PublicCharger: Decodable {
let AddressInfo: [AddressInfo]
}
Here is my code:
//Find public chargers from local coordinates
func findPublicChargers(lat: Double, long: Double) {
//Use apiurl to pull all charge points that are currently in that area by adding lat and long into the api call &latitude=***&longitude=*****
let apiurl = "https://api.openchargemap.io/v3/poi/?output=json&countrycode=UK&maxresults=100&compact=true&verbose=false"
let urlString = "\(apiurl)&latitude=\(lat)&longitude=\(long)"
//print(urlString)
performRequest(urlString: urlString)
}
//Perform API Request - (London App Brewry code)
//Create the custom url
func performRequest(urlString: String) {
if let url = URL(string: urlString) {
//print("Called")
//Create a URL Session
let session = URLSession(configuration: .default)
//Give the session a task
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
//let dataString = String(data: safeData, encoding: .utf8)
//print(dataString)
self.parseJSON(data: safeData)
print("Data: \(safeData)")
}
}
//Start the task
task.resume()
}
}
func parseJSON(data: Data){
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(PublicCharger.self, from: data)
print("Data: \(decodedData.AddressInfo[0].Title)")
} catch {
print("Error!: \(error)")
}
}
struct AddressInfo: Decodable {
let Title: String
}
I have seen that in the data model you would need to include an unkeyed container element. I'm just not sure how this should be carried out in the data model. Any light on this would be much appreciated.
Try to change your PublicCharger data model to
struct PublicCharger: Decodable {
let AddressInfo: [AddressInfo]
}
And your parseJSON function to
func parseJSON(data: Data){
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode([PublicCharger].self, from: data)
if !decodedData.isEmpty {
print("Data: \(decodedData[0].AddressInfo[0].Title)")
} else {
print("Empty result!")
}
} catch {
print("Error!: \(error)")
}
}

Issue parsing the json response in swift

I have a json response as follows:
[
{
"item_id": 3310,
"sku": "BWBCL14KWGF003-BWBCL14KWGF003",
"qty": 1,
"name": "BWBCL14KWGF003",
"price": 471,
"product_type": "simple",
"quote_id": "4246",
"product_option": {
"extension_attributes": {
"custom_options": [
{
"option_id": "23243",
"option_value": "625080"
},
{
"option_id": "23242",
"option_value": "625032"
}
]
}
}
}
]
I have the alamofire code to get this response.
AF.request("https://adamas-intl.com/rest/V1/carts/mine/items", method: .get, parameters: nil, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
switch response.result {
case .success(let json):
if let res = json as? [[String: Any]]{
print("res is",res)
}
case let .failure(error):
print(error)
}
I need to fetch the item_id and other values from the response.This way of fetching,iam not able to reach inside the values.
How could i parse this json response?
I think that the best way here is to use a Decodable protocol.
struct Item: Decodable {
var itemId: Int
var sku: String
// ...
}
Then use responseDecodable(_:) method
// create a decoder to handle the `snakeCase` to `camelCase` attributes
// thanks to this `Decoder`, you are able to add a property `var itemId: Int` instead of `var item_id: Int`
let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
AF.request("https://adamas-intl.com/rest/V1/carts/mine/items")
.validate()
.responseDecodable(of: [Item].self, decoder: decoder) { (response) in
guard let items = response.value else { return }
// do what you want
}

Trying to parse json for public git repos in swift but receiving "Expected to decode Dictionary<String, Any> but found an array instead."

My json looks like this:
[
{
"name": "sensei",
"owner": {
"login": "linkedin",
},
"description": "distributed realtime searchable database",
"fork": false,
},
{
"name": "linkedin-utils",
"owner": {
"login": "linkedin",
},
"description": "Base utilities shared by all linkedin open source projects",
"fork": false,
}
]
The structs I built are the following:
struct LinkedinData: Codable {
var name: String
var description: String
var owner: OwnerLogin
var fork: Bool
}
struct OwnerLogin: Codable {
var login: String
}
My code for parsing is this one:
import UIKit
class ViewController: UIViewController {
var linkedinData = [LinkedinData]()
override func viewDidLoad() {
super.viewDidLoad()
let urString : String = "https://api.github.com/orgs/linkedin/repos"
if let url = URL(string: urString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return //exit out of function
}
parseJSON(json: data!)
}
task.resume()
}
func parseJSON(json: Data) {
let decoder = JSONDecoder()
if let decodedData = try? decoder.decode(LinkedinData.self, from: json) {
linkedinData = [decodedData]
}
}
}
I tried for hours bĂșt it seems impossible to parse the json and retreive the data I am looking for (name, description, owner.login and fork) in a collection type. Could you please help?
You should decode an array of LinkedinData, instead of just one, because your JSON has an array as its root:
[ <------- this "[" indicates an array
{
"name": "sensei",
"owner": {
"login": "linkedin",
},
Therefore, you should write:
if let decodedData = try? decoder.decode([LinkedinData].self, from: json) {
linkedinData = decodedData
}
if let decodedData = try? decoder.decode(LinkedinData.self, from: json) {
linkedinData = [decodedData]
}
replace this with
if let decodedData = try? decoder.decode([LinkedinData].self, from: json) {
linkedinData = decodedData
}
as your topmost object in JSON is an Array.

How to create a JSON from Dictionary with an array

I have an object "itensList", it has the fields "name", "createdAt" and an array of "itens".
I want to be able to build JSON that looks like this:
{
"name": "List name"
"CreatedAt": "12:12 12/12/2016"
"itens": [
{
"title": "Item title"
"CreatedAt": "12:13 12/12/2016"
"isDone": false
},
{
"title": "Another item title"
"CreatedAt": "12:14 12/12/2016"
"isDone": true
}
]
}
I have tried a few different approaches with no success.
Item Object
class Item: Object {
dynamic var name = ""
dynamic var createdAt = NSDate()
dynamic var isDone = false
}
Item List Object
class ItemList: Object {
dynamic var name = ""
dynamic var createdAt = NSDate()
let itens = List<Item>()
}
For the example, let's make an object similar to what you must have:
class Iten {
let title:String
let createdAt:String
let isDone:Bool
init(title: String, createdAt: String, isDone: Bool) {
self.title = title
self.createdAt = createdAt
self.isDone = isDone
}
}
The trick I suggest is to add a computed value that will return a dictionary:
class Iten {
let title:String
let createdAt:String
let isDone:Bool
init(title: String, createdAt: String, isDone: Bool) {
self.title = title
self.createdAt = createdAt
self.isDone = isDone
}
var toDictionary: [String:AnyObject] {
return ["title": title, "createdAt": createdAt, "isDone": isDone]
}
}
Let's use it:
let iten1Dict = Iten(title: "title1", createdAt: "date1", isDone: false).toDictionary
let iten2Dict = Iten(title: "title2", createdAt: "date2", isDone: true).toDictionary
We now make the encapsulating dictionary:
let dict: [String:AnyObject] = ["name": "List name", "createdAt": "dateX", "itens": [iten1Dict, iten2Dict]]
To finish, we encode this dictionary to JSON data then we decode it as a String:
do {
let jsonData = try NSJSONSerialization.dataWithJSONObject(dict, options: .PrettyPrinted)
if let jsonString = String(data: jsonData, encoding: NSUTF8StringEncoding) {
print(jsonString)
}
} catch let error as NSError {
print(error)
}
And voilĂ :
{
"createdAt" : "dateX",
"itens" : [
{
"title" : "title1",
"createdAt" : "date1",
"isDone" : false
},
{
"title" : "title2",
"createdAt" : "date2",
"isDone" : true
}
],
"name" : "List name"
}
Raphael,
This piece of code builds a JSON query. It should get you started, just keep hacking and you'll find a way! That's the fun of programming!
func JSONquery()
let request = NSMutableURLRequest(URL: NSURL(string: "https://api.dropboxapi.com/2/files/get_metadata")!)
let session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
request.addValue("application/json",forHTTPHeaderField: "Content-Type")
request.addValue("path", forHTTPHeaderField: lePath)
let cursor:NSDictionary? = ["path":lePath]
do {
let jsonData = try NSJSONSerialization.dataWithJSONObject(cursor!, options: [])
request.HTTPBody = jsonData
print("json ",jsonData)
} catch {
print("snafoo alert")
}
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
if let error = error {
completion(string: nil, error: error)
return
}
let strData = NSString(data: data!, encoding: NSUTF8StringEncoding)
//print("Body: \(strData)\n\n")
do {
let jsonResult = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers);
self.jsonPParser(jsonResult,field2file: "ignore")
/*for (key, value) in self.parsedJson {
print("key2 \(key) value2 \(value)")
}*/
completion(string: "", error: nil)
} catch {
completion(string: nil, error: error)
}
})
task.resume()
}
Like this:
var item = [
"title": "Item title",
"CreatedAt": "12:13 12/12/2016",
"isDone": false
]
var mainDictionary = [
"name": "List name",
"CreatedAt": "12:12 12/12/2016",
"items": [item]
]
And the just convert to json with NSJSONSerialization like this:
do {
let json = try NSJSONSerialization.dataWithJSONObject(mainDictionary, options: [])
} catch {
print(error)
}
UPDATE:
If you need to add values to array in dictionary you can do that like this:
if var items = mainDictionary["items"] as? NSMutableArray {
items.addObject(newItem)
mainDictionary["items"] = items
}