Parsing Nested JSON SWIFT 4 - ios

Could anyone tell me what I am doing wrong?
Here is JSON I'm trying to parse:
{
"results": {
"AF": {
"alpha3": "AFG",
"currencyId": "AFN",
"currencyName": "Afghan afghani",
"currencySymbol": "؋",
"id": "AF",
"name": "Afghanistan"
},
"AI": {
"alpha3": "AIA",
"currencyId": "XCD",
"currencyName": "East Caribbean dollar",
"currencySymbol": "$",
"id": "AI",
"name": "Anguilla"
}
}
}
My code :
class Results: Codable {
let results: [Country]
init(results: [Country]) {
self.results = results
}
}
class Country: Codable {
let currencyId: String
let currencyName: String
let currencySymbol: String
let id: String
let name: String
init(currencyId :String, currencyName: String, currencySymbol: String, id: String, name: String ) {
self.currencyId = currencyId
self.currencyName = currencyName
self.currencySymbol = currencySymbol
self.id = id
self.name = name
}
}
I have looked at Apple's Documentation on decoding nested structs, but I still do not understand how to do the different levels of the JSON properly.
Thanks.

Check the outlined value for the key "results".
"results": {
...
}
{...} represents JSON object. A Swift struct (or class if you think it's better) would be appropriate for JSON object in some cases.
In other cases, Swift Dictionary may be more appropriate.
And each value of this JSON object takes this form:
{
"alpha3": ...,
"currencyId": ...,
"currencyName": ...,
"currencySymbol": ...,
"id": ...,
"name": ...
}
which matches your Country.
So, you just need to change the type of results in your Results class.
class Results: Codable {
let results: [String: Country]
init(results: [String: Country]) {
self.results = results
}
}
Having the same name (without case) for a property and its class might cause some confusion in the future, but I keep it as is as for now.
You can test it like this:
(Assuming to be tested in the Playground with modified Results and your Country.)
let jsonText = """
{
"results": {
"AF": {
"alpha3": "AFG",
"currencyId": "AFN",
"currencyName": "Afghan afghani",
"currencySymbol": "؋",
"id": "AF",
"name": "Afghanistan"
},
"AI": {
"alpha3": "AIA",
"currencyId": "XCD",
"currencyName": "East Caribbean dollar",
"currencySymbol": "$",
"id": "AI",
"name": "Anguilla"
}
}
}
"""
let jsonData = jsonText.data(using: .utf8)!
let decoder = JSONDecoder()
do {
let results = try decoder.decode(Results.self, from: jsonData)
print(results) //-> __lldb_expr_1.Results
} catch {
print(error)
}

Related

How to pass a value while parsing a nested JSON in Swift

I would like to pass the parent area Id to it's children areas while parsing the nested JSON structure as per the attached response, Here I would like to insert 'parentId' for each children which will link to it's immediate parent area,
{
"areas": [
{
"id": "271341877549072423",
"name": "Breeze Office Tower",
"children": [
{
"id": "271341877549072424",
"name": "100 flinders street",
"position": 0,
"children": []
},
{
"id": "271341877549130929",
"name": "100 flinders street",
"position": 1,
"children": []
},
{
"id": "271341877549072425",
"name": "100 Flinder Stree",
"position": 2,
"children": [
{
"id": "271341877549072426",
"name": "Büro",
"position": 0,
"children": [
{
"id": "271341877549072427",
"name": "Dachgeschoß",
"position": 0,
"children": []
}
]
}
]
},
{
"id": "271341877549130931",
"name": "100 Flinder Stree",
"position": 3,
"children": [
{
"id": "271341877549130933",
"name": "Büro",
"position": 0,
"children": [
{
"id": "271341877549130935",
"name": "Dachgeschoß",
"position": 0,
"children": []
}
]
}
]
}
]
}
]
}
My JSON Codable model struct looks like,
struct AreaModel: Decodable {
var areas: [NestedAreaModel]?
}
struct NestedAreaModel: Codable {
let areaId: String
let areaName: String
let children: [NestedAreaModel]
let hasChildren: Bool
var areaPosition: Int16?
var parentId: String?
var projectId: String?
enum CodingKeys: String, CodingKey {
case areaId = "id"
case areaName = "name"
case areaPosition = "position"
case children
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.areaId = try values.decode(String.self, forKey: .areaId)
self.children = try values.decode([NestedAreaModel].self, forKey: .children)
self.areaName = try values.decode(String.self, forKey: .areaName)
self.projectId = ORAUserDefaults.selectedProjectId()
self.areaPosition = try values.decodeIfPresent(Int16.self, forKey: .areaPosition)
if !self.children.isEmpty {
self.hasChildren = true
self.parentId = self.areaId
} else {
self.hasChildren = false
}
}
}
Here I am not able to set the parent Id, its pointing its own id always.
As I allready pointed out in the comments you would need to iterate over the decoded children and set their respective parentId to the current areaId.
One possible Solution would be:
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.areaId = try values.decode(String.self, forKey: .areaId)
// decode the children to a local var
var children = try values.decode([NestedAreaModel].self, forKey: .children)
self.areaName = try values.decode(String.self, forKey: .areaName)
self.projectId = ORAUserDefaults.selectedProjectId()
self.areaPosition = try values.decodeIfPresent(Int16.self, forKey: .areaPosition)
// if there are children loop over them and assign your id
if !children.isEmpty {
self.hasChildren = true
for (index, _ ) in children.enumerated(){
children[index].parentId = areaId
}
} else {
self.hasChildren = false
}
// assign to self
self.children = children
}
Tested with the given JSON:
let decoded = try JSONDecoder().decode(AreaModel.self, from: data)
print(decoded.areas?[0].areaId)
print(decoded.areas?[0].children[0].parentId)
Result:
Optional("271341877549072423")
Optional("271341877549072423")
Remarks:
Regarding your comment on complexity:
children[index].parentId = areaId runs exactly once per child no matter of the level deapth. So this function is (O)n.

get position in a api swift Codable and Model data

i am getting data from an api like this
[
{
"internData": {
"id": "abc123",
"name": "Doctor"
},
"author": "Will smith",
"description": "Is an actor",
"url": "https://www",
},
{
"internData": {
"id": "qwe900",
"name": "Constructor"
},
"author": "Edd Bett",
"description": "Is an Constructor",
"url": "https://www3",
}
]
I have my model like this
struct PersonData: Codable {
let author: String?
let description: String?
let url: String?
}
But I dont know how to define the "internData", I tried with another Model "InterData" and define id and name like the PersonData, but i get an error, i tried also with [String:Any] but i get an error for the Codable protocol
I am using
let resP = try JSONSerialization.jsonObject(with: data, options: .init()) as? [String: AnyObject]
print("resP", )
in my script of Service/Network
Thanks if somebody knows
You can't use [String:Any] type in case of Codable. you need to create an another model of InternData, which is used by PersonData.
Code:
JSON Data :
let jsonData =
"""
[
{
"internData": {
"id": "abc123",
"name": "Doctor"
},
"author": "Will smith",
"description": "Is an actor",
"url": "https://www",
},
{
"internData": {
"id": "qwe900",
"name": "Constructor"
},
"author": "Edd Bett",
"description": "Is an Constructor",
"url": "https://www3",
}
]
"""
// Models
struct PersonData: Codable {
let author: String?
let description: String?
let url: String?
let internData : InternData?
}
// New model
struct InternData : Codable {
let id : String?
let name : String?
}
// Parsing
do {
let parseRes = try JSONDecoder().decode([PersonData].self, from: Data(jsonData.utf8))
print(parseRes)
}
catch {
print(error)
}

How do I use Codable to decode json response which has changing keys?

I have a json response from the fixer api. And I want to decode it to the struct which is shown below. The issue is the values of "rates" in the json is an array of key:value pairs, which has varying keys.
I have the second structure which must be mapped from these key-values.
How can i do it using Codable?
{
"success": true,
"timestamp": 1558883585,
"base": "EUR",
"date": "2019-05-26",
"rates": {
"AED": 4.116371,
"AFN": 90.160103,
"ALL": 122.341655,
"AMD": 536.359254,
"ANG": 2.097299,
"AOA": 368.543773,
"ARS": 50.418429,
"AUD": 1.618263,
"AWG": 2.012124,
"AZN": 1.910752,
"BAM": 1.955812,
"BBD": 2.227793,
"BDT": 94.245988,
"BGN": 1.956097,
"BHD": 0.421705,
"BIF": 2051.459244,
"BMD": 1.120649,
"BND": 1.539664,
"BOB": 7.729394,
"BRL": 4.508038,
"BSD": 1.118587,
"BTC": 0.00014,
"BTN": 77.755286,
"BWP": 12.040813,
"BYN": 2.323273,
"BYR": 21964.71167,
"BZD": 2.254689
}
}
If i change the 'ExchangeRate' struct as below, it is easily decoded. But I have the requirement where the "rates" should be an array of "ConvertedRate" struct.
struct ExchangeRate : Codable {
let base : String
let date : String
let rates : [String:Double]
}
This is what i need.
struct ExchangeRate : Codable {
let base : String
let date : String
let rates : [ConvertedRate] //keys are varied in json
}
struct ConvertedRate : Codable, Comparable{
let currencyName : String
let rate : Double
}
You'll have to write a custom init(from:) since you are not using the "default behavior".
More info on the Apple documentation.
Here is a possible solution:
struct ExchangeRate : Codable {
let base : String
let date : String
let rates : [ConvertedRate]
enum CodingKeys: String, CodingKey {
case base
case date
case rates
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
base = try values.decode(String.self, forKey: .base)
date = try values.decode(String.self, forKey: .date)
let additionalInfo = try values.decode([String: Double].self, forKey: .rates)
print(additionalInfo)
rates = additionalInfo.map({ ConvertedRate(name: $0.key, rate: $0.value) })
}
}
struct ConvertedRate : Codable {
let currencyName : String
let rate : Double
init(name: String, rate: Double) {
self.currencyName = name
self.rate = rate
}
}
Side Note: The current code can decode your JSON as you want, but it doesn't re-encoded as it was, because it's replicating the Swift structure:
{
"base": "EUR",
"date": "2019-05-26",
"rates": [{
"currencyName": "BSD",
"rate": 1.118587
}, {
"currencyName": "BWP",
"rate": 12.040813
}, {
"currencyName": "BYN",
"rate": 2.3232729999999999
}, {
"currencyName": "BBD",
"rate": 2.2277930000000001
}, {
"currencyName": "BOB",
"rate": 7.7293940000000001
}, {
"currencyName": "BAM",
"rate": 1.9558120000000001
}, {
"currencyName": "AUD",
"rate": 1.618263
}, {
"currencyName": "AFN",
"rate": 90.160103000000007
}, {
"currencyName": "BYR",
"rate": 21964.711670000001
}, {
"currencyName": "BRL",
"rate": 4.508038
}, {
"currencyName": "BMD",
"rate": 1.120649
}, {
"currencyName": "BGN",
"rate": 1.956097
}, {
"currencyName": "BHD",
"rate": 0.421705
}, {
"currencyName": "ANG",
"rate": 2.097299
}, {
"currencyName": "AOA",
"rate": 368.54377299999999
}, {
"currencyName": "BZD",
"rate": 2.2546889999999999
}, {
"currencyName": "ARS",
"rate": 50.418429000000003
}, {
"currencyName": "BTC",
"rate": 0.00013999999999999999
}, {
"currencyName": "BIF",
"rate": 2051.4592440000001
}, {
"currencyName": "AWG",
"rate": 2.012124
}, {
"currencyName": "AED",
"rate": 4.116371
}, {
"currencyName": "AMD",
"rate": 536.35925399999996
}, {
"currencyName": "BDT",
"rate": 94.245987999999997
}, {
"currencyName": "BND",
"rate": 1.5396639999999999
}, {
"currencyName": "BTN",
"rate": 77.755285999999998
}, {
"currencyName": "AZN",
"rate": 1.910752
}, {
"currencyName": "ALL",
"rate": 122.341655
}]
}
You have to write a custom initializer which maps the dictionary key-value pairs to an array of the custom struct.
It's not necessary to adopt Codable in CurrencyRate because the instances are created manually
let jsonString = """
{
"success": true,
"timestamp": 1558883585,
"base": "EUR",
"date": "2019-05-26",
"rates": {
"AED": 4.116371,"AFN": 90.160103,"ALL": 122.341655,"AMD": 536.359254,"ANG": 2.097299,"AOA": 368.543773,"ARS": 50.418429,"AUD": 1.618263,"AWG": 2.012124,"AZN": 1.910752,"BAM": 1.955812,"BBD": 2.227793,"BDT": 94.245988,"BGN": 1.956097,"BHD": 0.421705,"BIF": 2051.459244,"BMD": 1.120649,"BND": 1.539664,"BOB": 7.729394,"BRL": 4.508038,"BSD": 1.118587,"BTC": 0.00014,"BTN": 77.755286,"BWP": 12.040813,"BYN": 2.323273,"BYR": 21964.71167,"BZD": 2.254689
}
}
"""
struct ExchangeData : Codable {
let timestamp : Date
let base : String
let date : String
let rates : [CurrencyRate]
private enum CodingKeys: String, CodingKey { case timestamp, base, date, rates}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
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(CurrencyRate.init)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(timestamp, forKey: .timestamp)
try container.encode(base, forKey: .base)
try container.encode(date, forKey: .date)
var rateData = [String:Double]()
rates.forEach{ rateData[$0.name] = $0.rate }
try container.encode(rateData, forKey: .rates)
}
}
struct CurrencyRate {
let name : String
let rate : Double
}
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let result = try decoder.decode(ExchangeData.self, from: data)
print(result)
} catch {
print(error)
}

how can I filter student with all details whose "active" value is "true"?

I want to filter all details(school, city, name, active) whose active value is true. I have stored value of key "details"
let details = jsonRes[RequestResponses.Keys.details.rawValue] as? Dictionary< String, Any>
{
"details": {
"code": 235,
"school": "sp school",
"students": [
{ "name": "1student", "Active": false },
{ "name": "2student", "Active": true },
{ "name": "3student", "Active": true },
{ "name": "4student", "Active": false },
{ "name": "5student", "Active": false}
]
}
}
Expected Result
[
"details": {
"code": 235,
"school": "sp school",
"students": [
{ "name": "2student", "Active": true },
{ "name": "3student", "Active": true }
]
}
]
You can use filter
if let details = jsonRes[RequestResponses.Keys.details.rawValue] as? Dictionary< String, Any> ,
let detailDic = details["details"] as? [String:Any],
let students = detailDic["students"] as? [[String:Any]] {
let activeStudents = students.filter { (item) -> Bool in
guard let active = item["Active"] as? Bool else {return false}
return active
}
print(activeStudents)
}
or you can use shourthand
if let details = jsonRes[RequestResponses.Keys.details.rawValue] as? Dictionary< String, Any> ,
let detailDic = details["details"] as? [String:Any],
let students = detailDic["students"] as? [[String:Any]] {
let activeStudents = (details["students"] as?
[[String:Any]])?.filter{ $0["Active"] as? Bool == true}
print(activeStudents)
}
You start realising the true elegance of Swift once you start turning this into regular objects using Codable. This will let you do things as in the Playground:
import Cocoa
let jsonData = """
{
"details": {
"code": 235,
"school": "sp school",
"students": [
{ "name": "1student", "Active": false },
{ "name": "2student", "Active": true },
{ "name": "3student", "Active": true },
{ "name": "4student", "Active": false },
{ "name": "5student", "Active": false}
]
}
}
""".data(using: .utf8)!
struct Student : Codable {
let name: String
let active: Bool
enum CodingKeys: String, CodingKey {
case name
case active = "Active"
}
}
struct School : Codable {
let code: Int
let school: String
let students: [Student]
}
struct Details: Codable {
let details: School
}
do {
let det = try JSONDecoder().decode(Details.self, from: jsonData)
print(det)
let activeStudents = det.details.students.filter({(student)->Bool in student.active})
print(activeStudents)
} catch {
print(error)
}
This is obviously much easier to understand and Xcode can also support you much better during the process. The effort spent on the parser is minimal and easily recovered by the sheer elegance and clarity of the final filtering line.

Use Swifty JSON to parse array

This is my json to parse (example):
[
{
"id": 1,
"name": "Team name",
"shower": {
"id": 1,
"status": 1,
"startLocation": {
"id": 1,
"name": "abc 16"
}
}
},
{
"id": 2,
"name": "Team name",
"shower": {
"id": 2,
"status": 1,
"startLocation": {
"id": 1,
"name": "efg 16"
}
}
}
]
paste it this json viewer to view it easily.
as you can see, it is an array (of teams).
I need to get each team and do something with it.
After many attempts, I tried using SwiftyJSON, because I thought it will be easier. But, it did not worked for me.
This is what I tried:
let array = JSON(response)
// print each subJSON in array
for team in array.arrayValue {
print(team)
}
But the loop does not work. It does not go in to the loop at all.
Maybe it does not understand that my json is an array.
I can see the array object in the debugger. It looks like this:
How can I get these sub-JSONs?
Thanks.
I think you should use
let array = JSON(parseJSON: response)
instead of
let array = JSON(response)
SwiftyJSON contains methods to parse JSON string into a JSON object, check documentation
/**
Parses the JSON string into a JSON object
- parameter json: the JSON string
- returns: the created JSON object
*/
public init(parseJSON jsonString: String) {
if let data = jsonString.data(using: .utf8) {
self.init(data)
} else {
self.init(NSNull())
}
}
/**
Creates a JSON from JSON string
- parameter string: Normal json string like '{"a":"b"}'
- returns: The created JSON
*/
#available(*, deprecated: 3.2, message: "Use instead `init(parseJSON: )`")
public static func parse(json: String) -> JSON {
return json.data(using: String.Encoding.utf8)
.flatMap{ JSON(data: $0) } ?? JSON(NSNull())
}
or alternatively you can convert son string into son object like
Swift 3:
let dataFromString = response.data(using: .utf8)
let jsonArray = JSON(data: dataFromString!)
In the following example, I save team names in an array. I've tested it.
var names = [String]()
if let array = json.array {
for i in 0..<array.count {
let name = array[i]["name"]
names.append(name.stringValue)
}
}
print(names) // ["Team name", "Team name"]
Here is the answer for Swift 5. In My case data response is something like below :
[
{
"Name": "Some Type",
"Data": [
{
"ParentId": 111,
"Code": "Personal",
"SortOrder": 1,
"Name": "Personal",
"Id": 323
},
{
"ParentId": null,
"Code": "Work",
"SortOrder": 2,
"Name": "Work",
"Id": 324
}
],
"KeyType": "Integer"
},
{
"Name": "Phone Type",
"Data": [
{
"ParentId": null,
"Code": "Phone",
"SortOrder": 1,
"Name": "Phone",
"Id": 785
},
{
"ParentId": null,
"Code": "Cell",
"SortOrder": 2,
"Name": "Cell",
"Id": 786
},
{
"ParentId": null,
"Code": "Fax",
"SortOrder": 3,
"Name": "Fax",
"Id": 787
},
{
"ParentId": null,
"Code": "Home",
"SortOrder": 4,
"Name": "Home",
"Id": 788
},
{
"ParentId": null,
"Code": "Office",
"SortOrder": 5,
"Name": "Office",
"Id": 789
}
],
"KeyType": "Integer"
}
]
I was handled it with following code.
struct responseObjectClass:BaseModel {
var responsearray: [arrayData]? = nil
init(json: JSON) {
responsearray = json.arrayValue.map { arrayData(json: $0) }
}
}
struct arrayData:BaseModel {
let Name: String?
var DataValue: [DataLookup]? = nil
let KeyType: String?
init(json: JSON) {
Name = json["Name"].stringValue
DataValue = json["Data"].arrayValue.map { DataLookup(json: $0) }
KeyType = json["KeyType"].stringValue
}
}
struct DataLookup:BaseModel {
let ParentId: Any?
let Code: String?
let SortOrder: Int?
let Name: String?
let Id: Int?
init(json: JSON) {
ParentId = json["ParentId"]
Code = json["Code"].stringValue
SortOrder = json["SortOrder"].intValue
Name = json["Name"].stringValue
Id = json["Id"].intValue
}
}
BaseModel is Optional it's just used for init Json.
protocol BaseModel {
init(json: JSON)
}
Without SwiftyJSON
Below is the valid JSON
data.json File
[{
"id": 1,
"name": "Team name",
"shower": {
"id": 1,
"status": 1,
"startLocation": {
"id": 1,
"name": "abc 16"
}
}
}, {
"id": 2,
"name": "Team name",
"shower": {
"id": 2,
"status": 1,
"startLocation": {
"id": 1,
"name": "efg 16"
}
}
}]
Below is the code to read your json.
if let path = Bundle.main.path(forResource: "data", ofType: "json") {
let url = URL(fileURLWithPath: path)
do {
let data = try Data(contentsOf: url)
if let jsonArray = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? NSArray {
for (_, item) in jsonArray.enumerated() {
let itemDict = item as! NSDictionary
let id = itemDict["id"] as! Int
let name = itemDict["name"] as! String
let shower = itemDict["shower"] as! NSDictionary
let showerId = shower["id"] as! Int
let showerStatus = shower["status"] as! Int
let startLocation = shower["startLocation"] as! NSDictionary
let startLocationId = startLocation["id"] as! Int
let startLocationName = startLocation["name"] as! String
}
}
} catch {
print("Error: \(error.localizedDescription)")
}
}
this is what worked for me:
// Convert JSON to Array
func JSONToArray(_ json: String) -> Array<Any>? {
if let data = json.data(using: String.Encoding.utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? Array
} catch let error as NSError {
print(error)
}
}
return nil
}
After using this function i could loop trough the sub JSONs.
Thanks.

Resources